[php] 最佳實踐多語言網站



Answers

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

Question

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

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

我即將升級/重新開發我已經使用了很長一段時間的內容管理系統。 但是,我感覺多語言對這個系統是一個很大的改進。 在我沒有使用任何框架之前,我將為即將推出的項目使用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

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




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

假設你有這樣的文字:

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 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.




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/




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.




Related