php - 最佳實踐多語言網站





mysql localization internationalization multilingual (12)


I am not going to attempt to refine the answers already given. Instead I will tell you about the way my own OOP PHP framework handles translations.

Internally, my framework use codes like en, fr, es, cn and so on. An array holds the languages supported by the website: array('en','fr','es','cn') The language code is passed via $_GET (lang=fr) and if not passed or not valid, it is set to the first language in the array. So at any time during program execution and from the very beginning, the current language is known.

It is useful to understand the kind of content that needs to be translated in a typical application:

1) error messages from classes (or procedural code) 2) non-error messages from classes (or procedural code) 3) page content (usually store in a database) 4) site-wide strings (like website name) 5) script-specific strings

The first type is simple to understand. Basically, we are talking about messages like "could not connect to the database ...". These messages only need to be loaded when an error occurs. My manager class receives a call from the other classes and using the information passed as parameters simply goes to relevant the class folder and retrieves the error file.

The second type of error message is more like the messages you get when the validation of a form went wrong. ("You cannot leave ... blank" or "please choose a password with more than 5 characters"). The strings need to be loaded before the class runs.I know what is

For the actual page content, I use one table per language, each table prefixed by the code for the language. So en_content is the table with English language content, es_content is for spain, cn_content for China and fr_content is the French stuff.

The fourth kind of string is relevant throughout your website. This is loaded via a configuration file named using the code for the language, that is en_lang.php, es_lang.php and so on. In the global language file you will need to load the translated languages such as array('English','Chinese', 'Spanish','French') in the English global file and array('Anglais','Chinois', 'Espagnol', 'Francais') in the French file. So when you populate a dropdown for language selection, it is in the correct language ;)

Finally you have the script-specific strings. So if you write a cooking application, it might be "Your oven was not hot enough".

In my application cycle, the global language file is loaded first. In there you will find not just global strings (like "Jack's Website") but also settings for some of the classes. Basically anything that is language or culture-dependent. Some of the strings in there include masks for dates (MMDDYYYY or DDMMYYYY), or ISO Language Codes. In the main language file, I include strings for individual classes becaue there are so few of them.

The second and last language file that is read from disk is the script language file. lang_en_home_welcome.php is the language file for the home/welcome script. A script is defined by a mode (home) and an action (welcome). Each script has its own folder with config and lang files.

The script pulls the content from the database naming the content table as explained above.

If something goes wrong, the manager knows where to get the language-dependent error file. That file is only loaded in case of an error.

So the conclusion is obvious. Think about the translation issues before you start developing an application or framework. You also need a development workflow that incorporates translations. With my framework, I develop the whole site in English and then translate all the relevant files.

Just a quick final word on the way the translation strings are implemented. My framework has a single global, the $manager, which runs services available to any other service. So for example the form service gets hold of the html service and uses it to write the html. One of the services on my system is the translator service. $translator->set($service,$code,$string) sets a string for the current language. The language file is a list of such statements. $translator->get($service,$code) retrieves a translation string. The $code can be numeric like 1 or a string like 'no_connection'. There can be no clash between services because each has its own namespace in the translator's data area.

I post this here in the hope it will save somebody the task of reinventing the wheel like I had to do a few long years ago.

幾個月來,我一直在努力解決這個問題,但是我還沒有遇到過需要去探索所有可能的選擇的情況。 現在,我覺得是時候了解可能性並創建我自己的個人偏好以用於即將到來的項目。

首先讓我描繪一下我正在尋找的情況

我即將升級/重新開發我已經使用了很長一段時間的內容管理系統。 但是,我感覺多語言對這個系統是一個很大的改進。 在我沒有使用任何框架之前,我將為即將推出的項目使用Laraval4。 Laravel似乎是更清晰的PHP代碼的最佳選擇。 Sidenote: Laraval4 should be no factor in your answer 。 我正在尋找平台/框架無關的一般翻譯方式。

應該翻譯什麼

由於我所尋找的系統需要盡可能地方便用戶,管理翻譯的方法應該放在CMS內部。 應該不需要啟動FTP連接來修改翻譯文件或任何html / php解析的模板。

此外,我正在尋找翻譯多個數據庫表的最簡單方法,可能無需製作其他表格。

我自己想出了什麼

正如我一直在尋找,閱讀和嘗試自己已經。 我有幾個選項。 但我仍然不覺得我已經達到了我真正想要的最佳實踐方法。 現在,這就是我想到的,但這種方法也有副作用。

  1. PHP解析模板 :模板系統應該由PHP解析。 這樣我就可以將翻譯的參數插入到HTML中,而無需打開模板並修改它們。 除此之外,PHP解析模板使我能夠為整個網站提供1個模板,而不是為每種語言設置子文件夾(我曾經使用過)。 達到此目標的方法可以是Smarty,TemplatePower,Laravel's Blade或任何其他模板解析器。 正如我所說,這應該獨立於書面解決方案。
  2. 數據庫驅動 :也許我不需要再提及這一點。 但解決方案應該是數據庫驅動的。 CMS的目標是面向對象和MVC,所以我需要為這些字符串設想一個邏輯數據結構。 由於我的模板是結構化的:templates / Controller / View.php,這個結構可能是最有意義的: Controller.View.parameter 。 數據庫表將具有這些字段與value字段長。 在模板內部,我們可以使用一些排序方法,如echo __('Controller.View.welcome', array('name', 'Joshua')) ,參數包含Welcome, :name 。 因此, Welcome, Joshua的結果Welcome, Joshua 。 這似乎是一個很好的方法,因為諸如name之類的參數很容易被編輯理解。
  3. 低數據庫負載 :當然,如果這些字符串正在被加載,上述系統會導致數據庫負載的加載。 因此,我需要一個緩存系統,在編輯/保存在管理環境中後立即重新渲染語言文件。 由於生成文件,所以還需要一個好的文件系統佈局。 我想我們可以選擇languages/en_EN/Controller/View.php或.ini,無論你最適合。 也許.ini甚至更快地解析。 這個模塊應該包含format parameter=value;的數據format parameter=value; 。 我猜這是做這件事的最好方法,因為每個渲染的視圖如果存在的話可以包含它自己的語言文件。 語言參數應該加載到特定的視圖,而不是在全局範圍內,以防止參數互相覆蓋。
  4. 數據庫表翻譯 :這實際上是我最擔心的事情。 我正在尋找一種方法來創建新聞/網頁/等的翻譯。 盡快。 每個模塊都有兩個表格(例如NewsNews_translations )是一個選項,但是為了獲得一個好的系統感覺News_translations很多工作。 我想到的一件事是基於我寫的一個data versioning管理系統:有一個數據庫表名Translations ,這個表具有languagetablename和主鍵的獨特組合。 例如:en_En / News / 1(參考ID = 1的新聞項目英文版)。 但是這種方法有兩個巨大的缺點:首先,這個表格往往會在數據庫中有很多數據的情況下變得很長,其次,使用這個設置來搜索表格會是一個無聊的工作。 例如搜索該項目的SEO slu would將是一個全文搜索,這是非常愚蠢的。 但另一方面:這是一種在每個表格中快速創建可翻譯內容的快速方法,但我不認為這個專業人員勝過了這個con。
  5. 前端工作 :前端也需要一些思考。 當然,我們會將可用的語言存儲在數據庫中,然後激活我們需要的語言。 這樣腳本就可以生成一個下拉菜單來選擇一種語言,後端可以自動決定使用CMS進行哪些翻譯。 所選語言(例如en_EN)將在獲取視圖的語言文件或獲取網站上的內容項目的正確翻譯時使用。

所以,他們在那裡。 我的想法到目前為止。 他們甚至不包括日期等的本地化選項,但由於我的服務器支持PHP5.3.2 +,最好的選擇是使用intl擴展,如下所述: http://devzone.zend.com/1500/internationalization-in-php-53/ ://devzone.zend.com/1500/internationalization-in http://devzone.zend.com/1500/internationalization-in-php-53/ - 但這在任何後來的開發體育場都有用。 目前的主要問題是如何獲得網站內容翻譯的最佳實踐。

除了我在這裡解釋的所有內容之外,我還有一件我還沒有確定的東西,它看起來像一個簡單的問題,但實際上它讓我頭疼:

網址翻譯? 我們應該做還是不做? 以什麼方式?

所以..如果我有這個網址: http://www.domain.com/about-us : http://www.domain.com/about-us英語是我的默認語言。 當我選擇荷蘭語作為我的語言時,該網址是否應該翻譯成http://www.domain.com/over-ons ? 或者我們應該走簡單的路,只需更改/about可見的頁面/about 。 最後一件事似乎並不是一個有效的選擇,因為這會產生同一個URL的多個版本,這種索引內容的方式將失敗。

另一種選擇是使用http://www.domain.com/nl/about-us代替。 這會為每個內容至少生成一個唯一的URL。 而且這樣會更容易轉到另一種語言,例如http://www.domain.com/en/about-us並且為Google和人類訪問者提供的URL更易於理解。 使用這個選項,我們對默認語言做什麼? 默認語言是否應該刪除默認選擇的語言? 因此,將http://www.domain.com/en/about-us重定向到http://www.domain.com/about-us ...在我看來,這是最好的解決方案,因為當CMS設置為只有一種語言不需要在URL中具有該語言標識。

第三個選項是兩種選擇的組合:對主要語言使用“無語言標識”-URL( http://www.domain.com/about-us )。 並使用帶有翻譯的SEO slug的URL用於子語言: http://www.domain.com/nl/over-ons : http://www.domain.com/nl/over-ons : http://www.domain.com/de/uber-uns

我希望我的問題讓你的頭腦開裂,他們肯定會破壞我的! 它確實幫助我在這裡解決問題。 給我一個可能性來審查我以前使用的方法和我對即將到來的CMS的想法。

我想感謝您花時間閱讀這些文本!

// Edit #1

我忘了提及:__()函數是翻譯給定字符串的別名。 在這種方法中,顯然應該有某種回退方法,當沒有可用的翻譯時加載默認文本。 如果翻譯缺失,它應該被插入或翻譯文件應該重新生成。




Just a sub answer: Absolutely use translated urls with a language identifier in front of them: http://www.domain.com/nl/over-ons
Hybride solutions tend to get complicated, so I would just stick with it. 為什麼? Cause the url is essential for SEO.

About the db translation: Is the number of languages more or less fixed? Or rather unpredictable and dynamic? If it is fixed, I would just add new columns, otherwise go with multiple tables.

But generally, why not use Drupal? I know everybody wants to build their own CMS cause it's faster, leaner, etc. etc. But that is just really a bad idea!




A really simple option that works with any website where you can upload Javascript is www.multilingualizer.com

It lets you put all text for all languages onto one page and then hides the languages the user doesn't need to see. Works well.




Database work:

Create Language Table 'languages':

Fields:

language_id(primary and auto increamented)

language_name

created_at

created_by

updated_at

updated_by

Create a table in database 'content':

Fields:

content_id(primary and auto increamented)

main_content

header_content

footer_content

leftsidebar_content

rightsidebar_content

language_id(foreign key: referenced to languages table)

created_at

created_by

updated_at

updated_by

Front End Work:

When user selects any language from dropdown or any area then save selected language id in session like,

$_SESSION['language']=1;

Now fetch data from database table 'content' based on language id stored in session.

Detail may found here http://skillrow.com/multilingual-website-in-php-2/




I've been asking myself related questions over and over again, then got lost in formal languages... but just to help you out a little I'd like to share some findings:

I recommend to give a look at advanced CMS

Typo3 for PHP (I know there is a lot of stuff but thats the one I think is most mature)

Plone in Python

If you find out that the web in 2013 should work different then, start from scratch. That would mean to put together a team of highly skilled/experienced people to build a new CMS. May be you'd like to give a look at polymer for that purpose.

If it comes to coding and multilingual websites / native language support, I think every programmer should have a clue about unicode. If you don't know unicode you'll most certainly mess up your data. Do not go with the thousands of ISO codes. They'll only save you some memory. But you can do literally everything with UTF-8 even store chinese chars. But for that you'd need to store either 2 or 4 byte chars that makes it basically a utf-16 or utf-32.

If it's about URL encoding, again there you shouldn't mix encodings and be aware that at least for the domainname there are rules defined by different lobbies that provide applications like a browser. eg a Domain could be very similar like:

ьankofamerica.com or bankofamerica.com samesamebutdifferent ;)

Of course you need the filesystem to work with all encodings. Another plus for unicode using utf-8 filesystem.

If its about translations, think about the structure of documents. eg a book or an article. You have the docbook specifications to understand about those structures. But in HTML its just about content blocks. So you'd like to have a translation on that level, also on webpage level or domain level. So if a block doesn't exist its just not there, if a webpage doesn't exist you'll get redirected to the upper navigation level. If a domain should be completely different in navigation structure, then.. its a complete different structure to manage. This can already be done with Typo3.

If its about frameworks, the most mature ones I know, to do the general stuff like MVC(buzzword I really hate it! Like "performance" If you want to sell something, use the word performance and featurerich and you sell... what the hell) is Zend . It has proven to be a good thing to bring standards to php chaos coders. But, typo3 also has a Framework besides the CMS. Recently it has been redeveloped and is called flow3 now. The frameworks of course cover database abstraction, templating and concepts for caching, but have individual strengths.

If its about caching... that can be awefully complicated / multilayered. In PHP you'll think about accellerator, opcode, but also html, httpd, mysql, xml, css, js ... any kinds of caches. Of course some parts should be cached and dynamic parts like blog answers shouldn't. Some should be requested over AJAX with generated urls. JSON, hashbangs etc.

Then, you'd like to have any little component on your website to be accessed or managed only by certain users , so conceptually that plays a big role.

Also you'd like to make statistics , maybe have distributed system / a facebook of facebooks etc. any software to be built on top of your over the top cms ... so you need different type of databases inmemory, bigdata, xml, whatsoever.

well, I think thats enough for now. If you haven't heard of either typo3 / plone or mentioned frameworks, you have enough to study. On that path you'll find a lot of solutions for questions you haven't asked yet.

If then you think, lets make a new CMS because its 2013 and php is about to die anyway, then you r welcome to join any other group of developers hopefully not getting lost.

祝你好運!

And btw. how about people will not having any websites anymore in the future? and we'll all be on google+? I hope developers become a little more creative and do something usefull(to not be assimilated by the borgle)

//// Edit /// Just a little thought for your existing application:

If you have a php mysql CMS and you wanted to embed multilang support. you could either use your table with an aditional column for any language or insert the translation with an object id and a language id in the same table or create an identical table for any language and insert objects there, then make a select union if you want to have them all displayed. For the database use utf8 general ci and of course in the front/backend use utf8 text/encoding. I have used url path segments for urls in the way you already explaned like

domain.org/en/about you can map the lang ID to your content table. anyway you need to have a map of parameters for your urls so you'd like to define a parameter to be mapped from a pathsegment in your URL that would be eg

domain.org/en/about/employees/IT/administrators/

lookup configuration

pageid| url

1 | /about/employees/../..

1 | /../about/employees../../

map parameters to url pathsegment ""

$parameterlist[lang] = array(0=>"nl",1=>"en"); // default nl if 0
$parameterlist[branch] = array(1=>"IT",2=>"DESIGN"); // default nl if 0
$parameterlist[employertype] = array(1=>"admin",1=>"engineer"); //could be a sql result 

$websiteconfig[]=$userwhatever;
$websiteconfig[]=$parameterlist;
$someparameterlist[] = array("branch"=>$someid);
$someparameterlist[] = array("employertype"=>$someid);
function getURL($someparameterlist){ 
// todo foreach someparameter lookup pathsegment 
return path;
}

per say, thats been covered already in upper post.

And to not forget, you'd need to "rewrite" the url to your generating php file that would in most cases be index.php




我建議你不要真的依賴數據庫進行翻譯,它可能是一個非常混亂的任務,並且在數據編碼的情況下可能是一個極端的問題。

我以前遇到過類似的問題,並在課後寫下來解決我的問題

對象:Locale \ Locale

<?php

  namespace Locale;

  class Locale{

// Following array stolen from Zend Framework
public $country_to_locale = array(
    'AD' => 'ca_AD',
    'AE' => 'ar_AE',
    'AF' => 'fa_AF',
    'AG' => 'en_AG',
    'AI' => 'en_AI',
    'AL' => 'sq_AL',
    'AM' => 'hy_AM',
    'AN' => 'pap_AN',
    'AO' => 'pt_AO',
    'AQ' => 'und_AQ',
    'AR' => 'es_AR',
    'AS' => 'sm_AS',
    'AT' => 'de_AT',
    'AU' => 'en_AU',
    'AW' => 'nl_AW',
    'AX' => 'sv_AX',
    'AZ' => 'az_Latn_AZ',
    'BA' => 'bs_BA',
    'BB' => 'en_BB',
    'BD' => 'bn_BD',
    'BE' => 'nl_BE',
    'BF' => 'mos_BF',
    'BG' => 'bg_BG',
    'BH' => 'ar_BH',
    'BI' => 'rn_BI',
    'BJ' => 'fr_BJ',
    'BL' => 'fr_BL',
    'BM' => 'en_BM',
    'BN' => 'ms_BN',
    'BO' => 'es_BO',
    'BR' => 'pt_BR',
    'BS' => 'en_BS',
    'BT' => 'dz_BT',
    'BV' => 'und_BV',
    'BW' => 'en_BW',
    'BY' => 'be_BY',
    'BZ' => 'en_BZ',
    'CA' => 'en_CA',
    'CC' => 'ms_CC',
    'CD' => 'sw_CD',
    'CF' => 'fr_CF',
    'CG' => 'fr_CG',
    'CH' => 'de_CH',
    'CI' => 'fr_CI',
    'CK' => 'en_CK',
    'CL' => 'es_CL',
    'CM' => 'fr_CM',
    'CN' => 'zh_Hans_CN',
    'CO' => 'es_CO',
    'CR' => 'es_CR',
    'CU' => 'es_CU',
    'CV' => 'kea_CV',
    'CX' => 'en_CX',
    'CY' => 'el_CY',
    'CZ' => 'cs_CZ',
    'DE' => 'de_DE',
    'DJ' => 'aa_DJ',
    'DK' => 'da_DK',
    'DM' => 'en_DM',
    'DO' => 'es_DO',
    'DZ' => 'ar_DZ',
    'EC' => 'es_EC',
    'EE' => 'et_EE',
    'EG' => 'ar_EG',
    'EH' => 'ar_EH',
    'ER' => 'ti_ER',
    'ES' => 'es_ES',
    'ET' => 'en_ET',
    'FI' => 'fi_FI',
    'FJ' => 'hi_FJ',
    'FK' => 'en_FK',
    'FM' => 'chk_FM',
    'FO' => 'fo_FO',
    'FR' => 'fr_FR',
    'GA' => 'fr_GA',
    'GB' => 'en_GB',
    'GD' => 'en_GD',
    'GE' => 'ka_GE',
    'GF' => 'fr_GF',
    'GG' => 'en_GG',
    'GH' => 'ak_GH',
    'GI' => 'en_GI',
    'GL' => 'iu_GL',
    'GM' => 'en_GM',
    'GN' => 'fr_GN',
    'GP' => 'fr_GP',
    'GQ' => 'fan_GQ',
    'GR' => 'el_GR',
    'GS' => 'und_GS',
    'GT' => 'es_GT',
    'GU' => 'en_GU',
    'GW' => 'pt_GW',
    'GY' => 'en_GY',
    'HK' => 'zh_Hant_HK',
    'HM' => 'und_HM',
    'HN' => 'es_HN',
    'HR' => 'hr_HR',
    'HT' => 'ht_HT',
    'HU' => 'hu_HU',
    'ID' => 'id_ID',
    'IE' => 'en_IE',
    'IL' => 'he_IL',
    'IM' => 'en_IM',
    'IN' => 'hi_IN',
    'IO' => 'und_IO',
    'IQ' => 'ar_IQ',
    'IR' => 'fa_IR',
    'IS' => 'is_IS',
    'IT' => 'it_IT',
    'JE' => 'en_JE',
    'JM' => 'en_JM',
    'JO' => 'ar_JO',
    'JP' => 'ja_JP',
    'KE' => 'en_KE',
    'KG' => 'ky_Cyrl_KG',
    'KH' => 'km_KH',
    'KI' => 'en_KI',
    'KM' => 'ar_KM',
    'KN' => 'en_KN',
    'KP' => 'ko_KP',
    'KR' => 'ko_KR',
    'KW' => 'ar_KW',
    'KY' => 'en_KY',
    'KZ' => 'ru_KZ',
    'LA' => 'lo_LA',
    'LB' => 'ar_LB',
    'LC' => 'en_LC',
    'LI' => 'de_LI',
    'LK' => 'si_LK',
    'LR' => 'en_LR',
    'LS' => 'st_LS',
    'LT' => 'lt_LT',
    'LU' => 'fr_LU',
    'LV' => 'lv_LV',
    'LY' => 'ar_LY',
    'MA' => 'ar_MA',
    'MC' => 'fr_MC',
    'MD' => 'ro_MD',
    'ME' => 'sr_Latn_ME',
    'MF' => 'fr_MF',
    'MG' => 'mg_MG',
    'MH' => 'mh_MH',
    'MK' => 'mk_MK',
    'ML' => 'bm_ML',
    'MM' => 'my_MM',
    'MN' => 'mn_Cyrl_MN',
    'MO' => 'zh_Hant_MO',
    'MP' => 'en_MP',
    'MQ' => 'fr_MQ',
    'MR' => 'ar_MR',
    'MS' => 'en_MS',
    'MT' => 'mt_MT',
    'MU' => 'mfe_MU',
    'MV' => 'dv_MV',
    'MW' => 'ny_MW',
    'MX' => 'es_MX',
    'MY' => 'ms_MY',
    'MZ' => 'pt_MZ',
    'NA' => 'kj_NA',
    'NC' => 'fr_NC',
    'NE' => 'ha_Latn_NE',
    'NF' => 'en_NF',
    'NG' => 'en_NG',
    'NI' => 'es_NI',
    'NL' => 'nl_NL',
    'NO' => 'nb_NO',
    'NP' => 'ne_NP',
    'NR' => 'en_NR',
    'NU' => 'niu_NU',
    'NZ' => 'en_NZ',
    'OM' => 'ar_OM',
    'PA' => 'es_PA',
    'PE' => 'es_PE',
    'PF' => 'fr_PF',
    'PG' => 'tpi_PG',
    'PH' => 'fil_PH',
    'PK' => 'ur_PK',
    'PL' => 'pl_PL',
    'PM' => 'fr_PM',
    'PN' => 'en_PN',
    'PR' => 'es_PR',
    'PS' => 'ar_PS',
    'PT' => 'pt_PT',
    'PW' => 'pau_PW',
    'PY' => 'gn_PY',
    'QA' => 'ar_QA',
    'RE' => 'fr_RE',
    'RO' => 'ro_RO',
    'RS' => 'sr_Cyrl_RS',
    'RU' => 'ru_RU',
    'RW' => 'rw_RW',
    'SA' => 'ar_SA',
    'SB' => 'en_SB',
    'SC' => 'crs_SC',
    'SD' => 'ar_SD',
    'SE' => 'sv_SE',
    'SG' => 'en_SG',
    'SH' => 'en_SH',
    'SI' => 'sl_SI',
    'SJ' => 'nb_SJ',
    'SK' => 'sk_SK',
    'SL' => 'kri_SL',
    'SM' => 'it_SM',
    'SN' => 'fr_SN',
    'SO' => 'sw_SO',
    'SR' => 'srn_SR',
    'ST' => 'pt_ST',
    'SV' => 'es_SV',
    'SY' => 'ar_SY',
    'SZ' => 'en_SZ',
    'TC' => 'en_TC',
    'TD' => 'fr_TD',
    'TF' => 'und_TF',
    'TG' => 'fr_TG',
    'TH' => 'th_TH',
    'TJ' => 'tg_Cyrl_TJ',
    'TK' => 'tkl_TK',
    'TL' => 'pt_TL',
    'TM' => 'tk_TM',
    'TN' => 'ar_TN',
    'TO' => 'to_TO',
    'TR' => 'tr_TR',
    'TT' => 'en_TT',
    'TV' => 'tvl_TV',
    'TW' => 'zh_Hant_TW',
    'TZ' => 'sw_TZ',
    'UA' => 'uk_UA',
    'UG' => 'sw_UG',
    'UM' => 'en_UM',
    'US' => 'en_US',
    'UY' => 'es_UY',
    'UZ' => 'uz_Cyrl_UZ',
    'VA' => 'it_VA',
    'VC' => 'en_VC',
    'VE' => 'es_VE',
    'VG' => 'en_VG',
    'VI' => 'en_VI',
    'VN' => 'vn_VN',
    'VU' => 'bi_VU',
    'WF' => 'wls_WF',
    'WS' => 'sm_WS',
    'YE' => 'ar_YE',
    'YT' => 'swb_YT',
    'ZA' => 'en_ZA',
    'ZM' => 'en_ZM',
    'ZW' => 'sn_ZW'
);

/**
 * Store the transaltion for specific languages
 *
 * @var array
 */
protected $translation = array();

/**
 * Current locale
 *
 * @var string
 */
protected $locale;

/**
 * Default locale
 *
 * @var string
 */
protected $default_locale;

/**
 *
 * @var string
 */
protected $locale_dir;

/**
 * Construct.
 *
 *
 * @param string $locale_dir            
 */
public function __construct($locale_dir)
{
    $this->locale_dir = $locale_dir;
}

/**
 * Set the user define localte
 *
 * @param string $locale            
 */
public function setLocale($locale = null)
{
    $this->locale = $locale;

    return $this;
}

/**
 * Get the user define locale
 *
 * @return string
 */
public function getLocale()
{
    return $this->locale;
}

/**
 * Get the Default locale
 *
 * @return string
 */
public function getDefaultLocale()
{
    return $this->default_locale;
}

/**
 * Set the default locale
 *
 * @param string $locale            
 */
public function setDefaultLocale($locale)
{
    $this->default_locale = $locale;

    return $this;
}

/**
 * Determine if transltion exist or translation key exist
 *
 * @param string $locale            
 * @param string $key            
 * @return boolean
 */
public function hasTranslation($locale, $key = null)
{
    if (null == $key && isset($this->translation[$locale])) {
        return true;
    } elseif (isset($this->translation[$locale][$key])) {
        return true;
    }

    return false;
}

/**
 * Get the transltion for required locale or transtion for key
 *
 * @param string $locale            
 * @param string $key            
 * @return array
 */
public function getTranslation($locale, $key = null)
{
    if (null == $key && $this->hasTranslation($locale)) {
        return $this->translation[$locale];
    } elseif ($this->hasTranslation($locale, $key)) {
        return $this->translation[$locale][$key];
    }

    return array();
}

/**
 * Set the transtion for required locale
 *
 * @param string $locale
 *            Language code
 * @param string $trans
 *            translations array
 */
public function setTranslation($locale, $trans = array())
{
    $this->translation[$locale] = $trans;
}

/**
 * Remove transltions for required locale
 *
 * @param string $locale            
 */
public function removeTranslation($locale = null)
{
    if (null === $locale) {
        unset($this->translation);
    } else {
        unset($this->translation[$locale]);
    }
}

/**
 * Initialize locale
 *
 * @param string $locale            
 */
public function init($locale = null, $default_locale = null)
{
    // check if previously set locale exist or not
    $this->init_locale();
    if ($this->locale != null) {
        return;
    }

    if ($locale == null || (! preg_match('#^[a-z]+_[a-zA-Z_]+$#', $locale) && ! preg_match('#^[a-z]+_[a-zA-Z]+_[a-zA-Z_]+$#', $locale))) {
        $this->detectLocale();
    } else {
        $this->locale = $locale;
    }

    $this->init_locale();
}

/**
 * Attempt to autodetect locale
 *
 * @return void
 */
private function detectLocale()
{
    $locale = false;

    // GeoIP
    if (function_exists('geoip_country_code_by_name') && isset($_SERVER['REMOTE_ADDR'])) {

        $country = geoip_country_code_by_name($_SERVER['REMOTE_ADDR']);

        if ($country) {

            $locale = isset($this->country_to_locale[$country]) ? $this->country_to_locale[$country] : false;
        }
    }

    // Try detecting locale from browser headers
    if (! $locale) {

        if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {

            $languages = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);

            foreach ($languages as $lang) {

                $lang = str_replace('-', '_', trim($lang));

                if (strpos($lang, '_') === false) {

                    if (isset($this->country_to_locale[strtoupper($lang)])) {

                        $locale = $this->country_to_locale[strtoupper($lang)];
                    }
                } else {

                    $lang = explode('_', $lang);

                    if (count($lang) == 3) {
                        // language_Encoding_COUNTRY
                        $this->locale = strtolower($lang[0]) . ucfirst($lang[1]) . strtoupper($lang[2]);
                    } else {
                        // language_COUNTRY
                        $this->locale = strtolower($lang[0]) . strtoupper($lang[1]);
                    }

                    return;
                }
            }
        }
    }

    // Resort to default locale specified in config file
    if (! $locale) {
        $this->locale = $this->default_locale;
    }
}

/**
 * Check if config for selected locale exists
 *
 * @return void
 */
private function init_locale()
{
    if (! file_exists(sprintf('%s/%s.php', $this->locale_dir, $this->locale))) {
        $this->locale = $this->default_locale;
    }
}

/**
 * Load a Transtion into array
 *
 * @return void
 */
private function loadTranslation($locale = null, $force = false)
{
    if ($locale == null)
        $locale = $this->locale;

    if (! $this->hasTranslation($locale)) {
        $this->setTranslation($locale, include (sprintf('%s/%s.php', $this->locale_dir, $locale)));
    }
}

/**
 * Translate a key
 *
 * @param
 *            string Key to be translated
 * @param
 *            string optional arguments
 * @return string
 */
public function translate($key)
{
    $this->init();
    $this->loadTranslation($this->locale);

    if (! $this->hasTranslation($this->locale, $key)) {

        if ($this->locale !== $this->default_locale) {

            $this->loadTranslation($this->default_locale);

            if ($this->hasTranslation($this->default_locale, $key)) {

                $translation = $this->getTranslation($this->default_locale, $key);
            } else {
                // return key as it is or log error here
                return $key;
            }
        } else {
            return $key;
        }
    } else {
        $translation = $this->getTranslation($this->locale, $key);
    }
    // Replace arguments
    if (false !== strpos($translation, '{a:')) {
        $replace = array();
        $args = func_get_args();
        for ($i = 1, $max = count($args); $i < $max; $i ++) {
            $replace['{a:' . $i . '}'] = $args[$i];
        }
        // interpolate replacement values into the messsage then return
        return strtr($translation, $replace);
    }

    return $translation;
  }
}

用法

 <?php
    ## /locale/en.php

    return array(
       'name' => 'Hello {a:1}'
       'name_full' => 'Hello {a:1} {a:2}'
   );

$locale = new Locale(__DIR__ . '/locale');
$locale->setLocale('en');// load en.php from locale dir
//want to work with auto detection comment $locale->setLocale('en');

echo $locale->translate('name', 'Foo');
echo $locale->translate('name', 'Foo', 'Bar');

怎麼運行的

{a:1} is replaced by 1st argument passed to method Locale::translate('key_name','arg1') {a:2} is replaced by 2nd argument passed to method Locale::translate('key_name','arg1','arg2')

How detection works

  • By default if geoip is installed then it will return country code by geoip_country_code_by_name and if geoip is not installed the fallback to HTTP_ACCEPT_LANGUAGE header



As a person who live in Quebec where almost all site is french and english... i have try many if not most multilanguage plugin for WP... the one an only usefull solution that work nive with all my site is mQtranslate... i live and die with it !

https://wordpress.org/plugins/mqtranslate/




這取決於你的網站有多少內容。 起初,我在這裡使用了一個像所有其他人一樣的數據庫,但腳本中的所有數據庫操作可能非常耗時。 我並不是說這是一種理想的方法,尤其是如果你有很多文本,但是如果你想在不使用數據庫的情況下快速完成,這種方法可以工作,但是你不能允許用戶輸入數據這將被用作翻譯文件。 但是如果你自己添加翻譯,它將起作用:

假設你有這樣的文字:

Welcome!

你可以在一個帶有翻譯的數據庫中輸入它,但你也可以這樣做:

$welcome = array(
"English"=>"Welcome!",
"German"=>"Willkommen!",
"French"=>"Bienvenue!",
"Turkish"=>"Hoşgeldiniz!",
"Russian"=>"Добро пожаловать!",
"Dutch"=>"Welkom!",
"Swedish"=>"Välkommen!",
"Basque"=>"Ongietorri!",
"Spanish"=>"Bienvenito!"
"Welsh"=>"Croeso!");

現在,如果你的網站使用cookie,你有這樣的例子:

$_COOKIE['language'];

為了簡單起見,我們將它轉換成一個可以輕鬆使用的代碼:

$language=$_COOKIE['language'];

如果你的cookie語言是威爾士語,並且你有這樣的代碼:

echo $welcome[$language];

其結果是:

Croeso!

如果您需要為您的網站添加大量翻譯並且數據庫過於耗費,則使用陣列可能是理想的解決方案。




I had the same probem a while ago, before starting using Symfony framework.

  1. Just use a function __() which has arameters pageId (or objectId, objectTable described in #2), target language and an optional parameter of fallback (default) language. The default language could be set in some global config in order to have an easier way to change it later.

  2. For storing the content in database i used following structure: (pageId, language, content, variables).

    • pageId would be a FK to your page you want to translate. if you have other objects, like news, galleries or whatever, just split it into 2 fields objectId, objectTable.

    • language - obviously it would store the ISO language string EN_en, LT_lt, EN_us etc.

    • content - the text you want to translate together with the wildcards for variable replacing. Example "Hello mr. %%name%%. Your account balance is %%balance%%."

    • variables - the json encoded variables. PHP provides functions to quickly parse these. Example "name: Laurynas, balance: 15.23".

    • you mentioned also slug field. you could freely add it to this table just to have a quick way to search for it.

  3. Your database calls must be reduced to minimum with caching the translations. It must be stored in PHP array, because it is the fastest structure in PHP language. How you will make this caching is up to you. From my experience you should have a folder for each language supported and an array for each pageId. The cache should be rebuilt after you update the translation. ONLY the changed array should be regenerated.

  4. i think i answered that in #2

  5. your idea is perfectly logical. this one is pretty simple and i think will not make you any problems.

URLs should be translated using the stored slugs in the translation table.

Final words

it is always good to research the best practices, but do not reinvent the wheel. just take and use the components from well known frameworks and use them.

take a look at Symfony translation component . It could be a good code base for you.




按照Thomas Bley的建議使用預處理器實現i18n,而無需執行性能命中

在工作中,我們最近在我們的幾個物業上實施了國際化,我們一直在努力解決的問題是處理即時翻譯的性能問題,然後我發現了Thomas Bley寫的這篇很棒的博客文章這激發了我們使用i18n處理大流量負載時性能問題最少的方式。

我們不用為每個翻譯操作調用函數,而這正如我們在PHP中所了解的那樣昂貴,我們使用佔位符來定義我們的基本文件,然後使用預處理器來緩存這些文件(我們存儲文件修改時間以確保我們正在服務在任何時候的最新內容)。

翻譯標籤

Thomas使用{tr}{/tr}標籤來定義翻譯的開始和結束位置。 由於我們使用的是TWIG,因此我們不希望使用{以避免混淆,因此我們使用[%tr%][%/tr%]來代替。 基本上,這看起來像這樣:

`return [%tr%]formatted_value[%/tr%];`

請注意Thomas建議在文件中使用基礎英語。 我們不這樣做,因為如果我們改變英文的值,我們不想修改所有的翻譯文件。

INI文件

然後,我們為每種語言創建一個INI文件,格式為placeholder = translated

// lang/fr.ini
formatted_value = number_format($value * Model_Exchange::getEurRate(), 2, ',', ' ') . '€'

// lang/en_gb.ini
formatted_value = '£' . number_format($value * Model_Exchange::getStgRate())

// lang/en_us.ini
formatted_value = '$' . number_format($value)

允許用戶在CMS內部修改這些內容是很簡單的,只需通過\n=上的preg_split獲取密鑰對,並使CMS能夠寫入INI文件即可。

預處理器組件

本質上,托馬斯建議使用這樣的即時“編譯器”(但實際上,它是一個預處理器)函數來獲取您的翻譯文件並在磁盤上創建靜態PHP文件。 這樣,我們基本上緩存了我們翻譯的文件,而不是為文件中的每個字符串調用翻譯函數:

// This function was written by Thomas Bley, not by me
function translate($file) {
  $cache_file = 'cache/'.LANG.'_'.basename($file).'_'.filemtime($file).'.php';
  // (re)build translation?
  if (!file_exists($cache_file)) {
    $lang_file = 'lang/'.LANG.'.ini';
    $lang_file_php = 'cache/'.LANG.'_'.filemtime($lang_file).'.php';

    // convert .ini file into .php file
    if (!file_exists($lang_file_php)) {
      file_put_contents($lang_file_php, '<?php $strings='.
        var_export(parse_ini_file($lang_file), true).';', LOCK_EX);
    }
    // translate .php into localized .php file
    $tr = function($match) use (&$lang_file_php) {
      static $strings = null;
      if ($strings===null) require($lang_file_php);
      return isset($strings[ $match[1] ]) ? $strings[ $match[1] ] : $match[1];
    };
    // replace all {t}abc{/t} by tr()
    file_put_contents($cache_file, preg_replace_callback(
      '/\[%tr%\](.*?)\[%\/tr%\]/', $tr, file_get_contents($file)), LOCK_EX);
  }
  return $cache_file;
}

注意:我沒有驗證這個正則表達式的工作原理,我沒有從我們的公司服務器上複製它,但是你可以看到這個操作是如何工作的。

如何調用它

再一次,這個例子來自Thomas Bley,而不是來自我:

// instead of
require("core/example.php");
echo (new example())->now();

// we write
define('LANG', 'en_us');
require(translate('core/example.php'));
echo (new example())->now();

我們將語言存儲在cookie中(如果我們無法獲取cookie,則會將其存儲在會話變量中),然後在每個請求中檢索它。 您可以將它與一個可選的$_GET參數結合來覆蓋該語言,但我不建議每個語言的子域名或每頁語言,因為這會使得難以發現哪些頁面很受歡迎並會降低該值的入站鏈接,因為你會讓它們更難以傳播。

為什麼使用這種方法?

我們喜歡這種預處理方法,原因有三:

  1. 由於不會為很少變化的內容調用一大堆函數,因此巨大的性能提升(使用此系統,法語中的10萬用戶仍然只能運行一次翻譯替換)。
  2. 它不會為我們的數據庫增加任何負載,因為它使用簡單的平面文件並且是純PHP解決方案。
  3. 在我們的翻譯中使用PHP表達式的能力。

獲取翻譯的數據庫內容

我們只在數據庫中添加一個名為language內容列,然後我們使用前面定義的LANG常量的訪問器方法,所以我們的SQL調用(很遺憾地使用ZF1)如下所示:

$query = select()->from($this->_name)
                 ->where('language = ?', User::getLang())
                 ->where('id       = ?', $articleId)
                 ->limit(1);

我們的文章具有idlanguage的複合主鍵,因此第54可以存在於所有語言中。 如果未指定,我們的LANG默認為en_US

URL Slug Translation

我在這裡結合了兩件事,一個是引導程序中的一個函數,它接受語言的$_GET參數並覆蓋cookie變量,另一個是接受多個slug的路由。 然後你可以在你的路由中做這樣的事情:

"/wilkommen" => "/welcome/lang/de"
... etc ...

這些可以存儲在一個平面文件,可以很容易地從您的管理面板寫入。 JSON或XML可能為支持它們提供了一個好的結構。

有關其他選項的注意事項

基於PHP的即時翻譯

我看不出這些與預處理翻譯相比有什麼優勢。

基於前端的翻譯

我很早就發現這些有趣的事情,但是有一些警告。 例如,您必須向用戶提供您計劃翻譯的網站上的整個短語列表,如果您隱藏或禁止他們訪問的網站區域存在問題,這可能會有問題。

你還必須假設你的所有用戶都願意並且能夠在你的網站上使用Javascript,但是從我的統計數據來看,大約有2.5%的用戶沒有使用它(或者使用Noscript阻止我們的網站使用它) 。

數據庫驅動的翻譯

PHP的數據庫連接速度沒有什麼可寫的,這增加了調用每個要翻譯的短語的功能的高昂開銷。 這種方法的性能和可伸縮性問題似乎令人難以置信。




我建議你不要發明一個輪子,並使用gettext和ISO語言縮寫列表。 你看過i18n / l10n在流行的CMSes或框架中的實現嗎?

使用gettext,你將擁有一個功能強大的工具,其中很多情況已經像複數形式的數字一樣實現了。 在英語中,你只有2種選擇:單數和復數。 但在俄羅斯,例如有三種形式,並不像英文那麼簡單。

還有許多翻譯者已經有了​​使用gettext的經驗。

看看CakePHPDrupal 。 啟用多語言。 CakePHP作為界面本地化的例子,Drupal作為內容翻譯的例子。

對於l10n使用數據庫根本就不是這種情況。 這將是噸查詢。 標準方法是在早期階段(或者如果您更喜歡延遲加載,首次調用i10n函數時)獲取所有l10n數據。 它可以從.po文件讀取數據或從DB讀取所有數​​據。 而不僅僅是從數組中讀取請求的字符串。

如果您需要實現在線工具來翻譯界面,您可以將所有數據保存在數據庫中,但仍然可以將所有數據保存到文件以使用它。 為了減少內存中的數據量,您可以將所有已翻譯的消息/字符串拆分為組,並且只加載所需的組(如果可能的話)。

所以你完全適合你的#3。 有一個例外:通常它是一個大文件而不是每個控制器文件,或者如此。 因為打開一個文件對於性能來說是最好的選擇。 您可能知道一些高負載的Web應用程序將所有PHP代碼編譯到一個文件中,以避免在調用include / require時執行文件操作。

關於網址。 谷歌間接建議使用翻譯:

清楚地表明法文內容: http://example.ca/fr/vélo-de-montagne.htmlhttp://example.ca/fr/vélo-de-montagne.html

此外,我認為你需要將用戶重定向到默認語言前綴,例如http://examlpe.com/about-us將重定向到http://examlpe.com/en/about-us但是,如果您的網站只使用一種語言,那麼您根本不需要前綴。

查看: http://de.audiomicro.com/anhanger-hit-auswirkungen-psychodrama-sound-effekte-836925 http://nl.audiomicro.com/aanhangwagen-hit-effect-psychodrama-geluidseffecten-836925 http://de.audiomicro.com/anhanger-hit-auswirkungen-psychodrama-sound-effekte-836925

翻譯內容是比較困難的任務。 我認為這將與不同類型的內容(例如文章,菜單項等)有所不同。但在#4中,您的方式是正確的。 看看Drupal有更多的想法。 它有足夠清晰的數據庫模式和足夠好的翻譯界面。 就像你創造文章並選擇語言一樣。 而且你以後可以將它翻譯成其他語言。

我認為這不是與URL slugs問題。 你可以為slu create創建單獨的表格,這將是正確的決定。 即使使用大量數據,也使用正確的索引來查詢表是不成問題的。 它不是全文搜索,而是字符串匹配,如果將使用varchar數據類型為slu and,並且您也可以在該字段上有索引。

PS抱歉,我的英語遠非完美。




看看這裡的結果(對於將PHP代碼放在JS代碼框中的黑客抱歉):

http://jsfiddle.net/newms87/h3b0a0ha/embedded/result/

結果:在PHP 5.4中,對於不同大小的數組, serialize()unserialize()都顯著更快。

我在真實世界的數據上做了一個測試腳本,用於比較json_encode和serialize,json_decode與反序列化。 該測試是在生產電子商務網站的緩存系統上運行的。 它只需要獲取緩存中的數據,並測試所有數據的編碼/解碼(或序列化/反序列化)的時間,然後將其放在一個易於查看的表格中。

我在PHP 5.4共享託管服務器上運行這個。

結果非常確定,對於這些大到小的數據集,序列化和反序列化都是明顯的贏家。 特別是對於我的用例,json_decode和unserialize對緩存系統來說是最重要的。 反串行化在這裡幾乎是一個無處不在的贏家。 它通常是json_decode的2到4倍(有時是6或7倍)。

有趣的是要注意@ peter-bailey的不同結果。

以下是用於生成結果的PHP代碼:

<?php

ini_set('display_errors', 1);
error_reporting(E_ALL);

function _count_depth($array)
{
    $count     = 0;
    $max_depth = 0;
    foreach ($array as $a) {
        if (is_array($a)) {
            list($cnt, $depth) = _count_depth($a);
            $count += $cnt;
            $max_depth = max($max_depth, $depth);
        } else {
            $count++;
        }
    }

    return array(
        $count,
        $max_depth + 1,
    );
}

function run_test($file)
{
    $memory     = memory_get_usage();
    $test_array = unserialize(file_get_contents($file));
    $memory     = round((memory_get_usage() - $memory) / 1024, 2);

    if (empty($test_array) || !is_array($test_array)) {
        return;
    }

    list($count, $depth) = _count_depth($test_array);

    //JSON encode test
    $start            = microtime(true);
    $json_encoded     = json_encode($test_array);
    $json_encode_time = microtime(true) - $start;

    //JSON decode test
    $start = microtime(true);
    json_decode($json_encoded);
    $json_decode_time = microtime(true) - $start;

    //serialize test
    $start          = microtime(true);
    $serialized     = serialize($test_array);
    $serialize_time = microtime(true) - $start;

    //unserialize test
    $start = microtime(true);
    unserialize($serialized);
    $unserialize_time = microtime(true) - $start;

    return array(
        'Name'                   => basename($file),
        'json_encode() Time (s)' => $json_encode_time,
        'json_decode() Time (s)' => $json_decode_time,
        'serialize() Time (s)'   => $serialize_time,
        'unserialize() Time (s)' => $unserialize_time,
        'Elements'               => $count,
        'Memory (KB)'            => $memory,
        'Max Depth'              => $depth,
        'json_encode() Win'      => ($json_encode_time > 0 && $json_encode_time < $serialize_time) ? number_format(($serialize_time / $json_encode_time - 1) * 100, 2) : '',
        'serialize() Win'        => ($serialize_time > 0 && $serialize_time < $json_encode_time) ? number_format(($json_encode_time / $serialize_time - 1) * 100, 2) : '',
        'json_decode() Win'      => ($json_decode_time > 0 && $json_decode_time < $serialize_time) ? number_format(($serialize_time / $json_decode_time - 1) * 100, 2) : '',
        'unserialize() Win'      => ($unserialize_time > 0 && $unserialize_time < $json_decode_time) ? number_format(($json_decode_time / $unserialize_time - 1) * 100, 2) : '',
    );
}

$files = glob(dirname(__FILE__) . '/system/cache/*');

$data = array();

foreach ($files as $file) {
    if (is_file($file)) {
        $result = run_test($file);

        if ($result) {
            $data[] = $result;
        }
    }
}

uasort($data, function ($a, $b) {
    return $a['Memory (KB)'] < $b['Memory (KB)'];
});

$fields = array_keys($data[0]);
?>

<table>
    <thead>
    <tr>
        <?php foreach ($fields as $f) { ?>
            <td style="text-align: center; border:1px solid black;padding: 4px 8px;font-weight:bold;font-size:1.1em"><?= $f; ?></td>
        <?php } ?>
    </tr>
    </thead>

    <tbody>
    <?php foreach ($data as $d) { ?>
        <tr>
            <?php foreach ($d as $key => $value) { ?>
                <?php $is_win = strpos($key, 'Win'); ?>
                <?php $color = ($is_win && $value) ? 'color: green;font-weight:bold;' : ''; ?>
                <td style="text-align: center; vertical-align: middle; padding: 3px 6px; border: 1px solid gray; <?= $color; ?>"><?= $value . (($is_win && $value) ? '%' : ''); ?></td>
            <?php } ?>
        </tr>
    <?php } ?>
    </tbody>
</table>




php mysql localization internationalization multilingual