php 最佳实践多语言网站
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/19249159/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Best practice multi language website
提问by Joshua - Pendo
I've been struggling with this question for quite some months now, but I haven't been in a situation that I needed to explore all possible options before. Right now, I feel like it's time to get to know the possibilities and create my own personal preference to use in my upcoming projects.
几个月来,我一直在为这个问题苦苦挣扎,但我以前从未遇到过需要探索所有可能选项的情况。现在,我觉得是时候了解可能性并创建我自己的个人偏好以在我即将进行的项目中使用。
Let me first sketch the situation I'm looking for
让我先勾勒出我正在寻找的情况
I'm about to upgrade/redevelop a content management system which I've been using for quite a while now. However, I'm feeling multi language is a great improvement to this system. Before I did not use any frameworks but I'm going to use Laraval4 for the upcoming project. Laravel seems the best choice of a cleaner way to code PHP. Sidenote: Laraval4 should be no factor in your answer
. I'm looking for general ways of translation that are platform/framework independent.
我即将升级/重新开发一个我已经使用了很长时间的内容管理系统。但是,我觉得多语言对这个系统来说是一个很大的改进。在我没有使用任何框架之前,我将在即将到来的项目中使用 Laraval4。Laravel 似乎是编写 PHP 代码的更简洁方式的最佳选择。Sidenote: Laraval4 should be no factor in your answer
. 我正在寻找独立于平台/框架的通用翻译方式。
What should be translated
应该翻译什么
As the system I am looking for needs to be as user friendly as possible the method of managing the translation should be inside the CMS. There should be no need to start up an FTP connection to modify translation files or any html/php parsed templates.
由于我正在寻找的系统需要尽可能用户友好,因此管理翻译的方法应该在 CMS 内部。应该不需要启动 FTP 连接来修改翻译文件或任何 html/php 解析模板。
Furthermore, I'm looking for the easiest way to translate multiple database tables perhaps without the need of making additional tables.
此外,我正在寻找最简单的方法来转换多个数据库表,而无需制作额外的表。
What did I come up with myself
我是怎么想出来的
As I've been searching, reading and trying things myself already. There are a couple of options I have. But I still don't feel like I've reached a best practice method for what I am really seeking. Right now, this is what I've come up with, but this method also has it side effects.
因为我一直在寻找、阅读和尝试自己的东西。我有几个选择。但我仍然不觉得我已经达到了我真正寻求的最佳实践方法。现在,这就是我想出的,但这种方法也有副作用。
- PHP Parsed Templates: the template system should be parsed by PHP. This way I'm able to insert the translated parameters into the HTML without having to open the templates and modify them. Besides that, PHP parsed templates gives me the ability to have 1 template for the complete website instead of having a subfolder for each language (which I've had before). The method to reach this target can be either Smarty, TemplatePower, Laravel's Blade or any other template parser. As I said this should be independent to the written solution.
- Database Driven: perhaps I don't need to mention this again. But the solution should be database driven. The CMS is aimed to be object oriented and MVC, so I would need to think of a logical data structure for the strings. As my templates would be structured: templates/Controller/View.php perhaps this structure would make the most sense:
Controller.View.parameter
. The database table would have these fields a long with avalue
field. Inside the templates we could use some sort method likeecho __('Controller.View.welcome', array('name', 'Joshua'))
and the parameter containsWelcome, :name
. Thus the result beingWelcome, Joshua
. This seems a good way to do this, because the parameters such as :name are easy to understand by the editor. - Low Database Load: Of course the above system would cause loads of database load if these strings are being loaded on the go. Therefore I would need a caching system that re-renders the language files as soon as they are edited/saved in the administration environment. Because files are generated, also a good file system layout is needed. I guess we can go with
languages/en_EN/Controller/View.php
or .ini, whatever suits you best. Perhaps an .ini is even parsed quicker in the end. This fould should contain the data in theformat parameter=value;
. I guess this is the best way of doing this, since each View that is rendered can include it's own language file if it exists. Language parameters then should be loaded to a specific view and not in a global scope to prevent parameters from overwriting each other. - Database Table translation: this in fact is the thing I'm most worried about. I'm looking for a way to create translations of News/Pages/etc. as quickly as possible. Having two tables for each module (for example
News
andNews_translations
) is an option but it feels like to much work to get a good system. One of the things I came up with is based on adata versioning
system I wrote: there is one database table nameTranslations
, this table has a unique combination oflanguage
,tablename
andprimarykey
. For instance: en_En / News / 1 (Referring to the English version of the News item with ID=1). But there are 2 huge disadvantages to this method: first of all this table tends to get pretty long with a lot of data in the database and secondly it would be a hell of a job to use this setup to search the table. E.g. searching for the SEO slug of the item would be a full text search, which is pretty dumb. But on the other hand: it's a quick way to create translatable content in every table very fast, but I don't believe this pro overweights the con's. - Front-end Work: Also the front-end would need some thinking. Of course we would store the available languages in a database and (de)active the ones we need. This way the script can generate a dropdown to select a language and the back-end can decide automatically what translations can be made using the CMS. The chosen language (e.g. en_EN) would then be used when getting the language file for a view or to get the right translation for a content item on the website.
- PHP Parsed Templates:模板系统应该由PHP解析。这样我就可以将翻译后的参数插入到 HTML 中,而无需打开模板并修改它们。除此之外,PHP 解析模板使我能够为整个网站拥有 1 个模板,而不是为每种语言(我以前拥有过)拥有一个子文件夹。达到这个目标的方法可以是 Smarty、TemplatePower、Laravel's Blade 或任何其他模板解析器。正如我所说,这应该独立于书面解决方案。
- 数据库驱动:也许我不需要再次提及这一点。但解决方案应该是数据库驱动的。CMS 旨在面向对象和 MVC,因此我需要考虑字符串的逻辑数据结构。因为我的模板是结构化的:templates/Controller/View.php 也许这个结构最有意义:
Controller.View.parameter
. 数据库表将有这些字段和一个value
字段。在模板中,我们可以使用某种排序方法,例如echo __('Controller.View.welcome', array('name', 'Joshua'))
,参数包含Welcome, :name
. 因此结果是Welcome, Joshua
。这似乎是一个很好的方法,因为诸如 :name 之类的参数很容易被编辑器理解。 - 低数据库负载:当然,如果在旅途中加载这些字符串,上述系统会导致数据库负载负载。因此,我需要一个缓存系统,在管理环境中编辑/保存语言文件后立即重新呈现语言文件。因为文件是生成的,所以还需要一个好的文件系统布局。我想我们可以选择
languages/en_EN/Controller/View.php
.ini 或 .ini,任何最适合您的。也许 .ini 最终解析得更快。此文件应包含format parameter=value;
. 我想这是最好的方法,因为渲染的每个视图都可以包含它自己的语言文件(如果存在)。然后应该将语言参数加载到特定视图而不是全局范围内,以防止参数相互覆盖。 - 数据库表翻译:这其实是我最担心的事情。我正在寻找一种方法来创建新闻/页面/等的翻译。尽快。每个模块有两个表(例如
News
和News_translations
)是一种选择,但要获得一个好的系统感觉需要做很多工作。我想出的一件事是基于data versioning
我写的一个系统:有一个数据库表名Translations
,这个表有一个独特的组合language
,tablename
和primarykey
. 例如:en_En / News / 1(指ID=1的英文版News item)。但是这种方法有两个巨大的缺点:首先,由于数据库中有大量数据,这个表往往会变得很长,其次,使用这种设置来搜索表将是一项艰巨的工作。例如,搜索项目的 SEO slug 将是全文搜索,这是非常愚蠢的。但另一方面:这是一种在每个表格中快速创建可翻译内容的快速方法,但我不相信这个专业人士会过分强调骗局。 - 前端工作:前端也需要一些思考。当然,我们会将可用语言存储在数据库中,并(停用)我们需要的语言。通过这种方式,脚本可以生成一个下拉列表来选择一种语言,后端可以自动决定使用 CMS 可以进行哪些翻译。然后,在获取用于查看的语言文件或获取网站内容项的正确翻译时,将使用所选语言(例如 en_EN)。
So, there they are. My ideas so far. They don't even include localization options for dates etc yet, but as my server supports PHP5.3.2+ the best option is to use the intl extension as explained here: http://devzone.zend.com/1500/internationalization-in-php-53/- but this would be of use in any later stadium of development. For now the main issue is how to have the best practics of translation of the content in a website.
所以,他们来了。到目前为止我的想法。它们甚至不包括日期等的本地化选项,但由于我的服务器支持 PHP5.3.2+,最好的选择是使用 intl 扩展名,如下所述:http://devzone.zend.com/1500/internationalization-in -php-53/- 但这将在任何后来的开发体育场中使用。目前的主要问题是如何获得网站内容翻译的最佳实践。
Besides everything I explained here, I still have another thing which I haven't decided yet, it looks like a simple question, but in fact it's been giving me headaches:
除了我在这里解释的所有内容之外,我还有一件事尚未决定,它看起来像一个简单的问题,但实际上它让我头疼:
URL Translation? Should we do this or not? and in what way?
网址翻译?我们应该这样做还是不这样做?以什么方式?
So.. if I have this url: http://www.domain.com/about-us
and English is my default language. Should this URL be translated into http://www.domain.com/over-ons
when I choose Dutch as my language? Or should we go the easy road and simply change the content of the page visible at /about
. The last thing doesn't seem a valid option because that would generate multiple versions of the same URL, this indexing the content will fail the right way.
所以..如果我有这个 url:http://www.domain.com/about-us
并且英语是我的默认语言。http://www.domain.com/over-ons
当我选择荷兰语作为我的语言时,这个 URL 应该被翻译成吗?或者我们应该走简单的路,简单地更改在/about
. 最后一件事似乎不是一个有效的选择,因为这会生成同一 URL 的多个版本,这种索引内容将以正确的方式失败。
Another option is using http://www.domain.com/nl/about-us
instead. This generates at least a unique URL for each content. Also this would be easier to go to another language, for example http://www.domain.com/en/about-us
and the URL provided is easier to understand for both Google and Human visitors. Using this option, what do we do with the default languages? Should the default language remove the language selected by default? So redirecting http://www.domain.com/en/about-us
to http://www.domain.com/about-us
... In my eyes this is the best solution, because when the CMS is setup for only one language there is no need to have this language identification in the URL.
另一种选择是使用http://www.domain.com/nl/about-us
。这至少会为每个内容生成一个唯一的 URL。此外,这会更容易转到另一种语言,例如http://www.domain.com/en/about-us
,提供的 URL 对 Google 和人类访问者来说更容易理解。使用此选项,我们如何处理默认语言?默认语言是否应该删除默认选择的语言?所以重定向http://www.domain.com/en/about-us
到http://www.domain.com/about-us
...在我看来这是最好的解决方案,因为当 CMS 只设置一种语言时,不需要在 URL 中有这种语言标识。
And a third option is a combination from both options: using the "language-identification-less"-URL (http://www.domain.com/about-us
) for the main language. And use an URL with a translated SEO slug for sublanguages: http://www.domain.com/nl/over-ons
& http://www.domain.com/de/uber-uns
第三个选项是两个选项的组合:使用“无语言标识”-URL ( http://www.domain.com/about-us
) 作为主要语言。并为子语言使用带有翻译的 SEO slug 的 URL:http://www.domain.com/nl/over-ons
&http://www.domain.com/de/uber-uns
I hope my question gets your heads cracking, they cracked mine for sure! It did help me already to work things out as a question here. Gave me a possibility to review the methods I've used before and the idea's I'm having for my upcoming CMS.
我希望我的问题能让你头脑清醒,他们肯定破解了我的!它确实帮助我在这里解决了问题。让我有机会回顾我以前使用过的方法以及我对即将到来的 CMS 的想法。
I would like to thank you already for taking the time to read this bunch of text!
我要感谢您花时间阅读这堆文字!
// Edit #1
:
// Edit #1
:
I forgot to mention: the __() function is an alias to translate a given string. Within this method there obviously should be some sort of fallback method where the default text is loaded when there are not translations available yet. If the translation is missing it should either be inserted or the translation file should be regenerated.
我忘了提及: __() 函数是翻译给定字符串的别名。在这个方法中,显然应该有某种回退方法,当没有可用的翻译时加载默认文本。如果缺少翻译,则应插入或重新生成翻译文件。
采纳答案by tere?ko
Topic's premise
话题的前提
There are three distinct aspects in a multilingual site:
多语言站点具有三个不同的方面:
- interface translation
- content
- url routing
- 界面翻译
- 内容
- 网址路由
While they all interconnected in different ways, from CMS point of view they are managed using different UI elements and stored differently. You seem to be confident in your implementation and understanding of the first two. The question was about the latter aspect - "URL Translation? Should we do this or not? and in what way?"
虽然它们都以不同的方式互连,但从 CMS 的角度来看,它们使用不同的 UI 元素进行管理并以不同的方式存储。您似乎对前两个的实施和理解充满信心。问题是关于后一方面—— “URL 翻译?我们应该这样做还是不这样做?以什么方式?”
What the URL can be made of?
URL 可以由什么组成?
A very important thing is, don't get fancy with IDN. Instead favor transliteration(also: transcription and romanization). While at first glance IDN seems viable option for international URLs, it actually does not work as advertised for two reasons:
非常重要的一点是,不要迷恋IDN。取而代之的是音译(也:转录和罗马化)。虽然乍一看 IDN 似乎是国际 URL 的可行选择,但实际上它并不像宣传的那样有效,原因有两个:
- some browsers will turn the non-ASCII chars like
'ч'
or'?'
into'%D1%87'
and'%C5%BE'
- if user has custom themes, the theme's font is very likely to not have symbols for those letters
- 某些浏览器会将非 ASCII 字符(如
'ч'
或)'?'
转换为'%D1%87'
和'%C5%BE'
- 如果用户有自定义主题,主题的字体很可能没有这些字母的符号
I actually tried to IDN approach few years ago in a Yii based project (horrible framework, IMHO). I encountered both of the above mentioned problems before scraping that solution. Also, I suspect that it might be an attack vector.
几年前,我实际上在一个基于 Yii 的项目(可怕的框架,恕我直言)中尝试使用 IDN 方法。在抓取该解决方案之前,我遇到了上述两个问题。此外,我怀疑它可能是一个攻击媒介。
Available options ... as I see them.
可用选项......正如我所见。
Basically you have two choices, that could be abstracted as:
基本上你有两个选择,可以抽象为:
http://site.tld/[:query]
: where[:query]
determines both language and content choicehttp://site.tld/[:language]/[:query]
: where[:language]
part of URL defines the choice of language and[:query]
is used only to identify the content
http://site.tld/[:query]
:[:query]
决定语言和内容选择的地方http://site.tld/[:language]/[:query]
:[:language]
URL 的一部分定义了语言的选择,[:query]
仅用于标识内容
Query is Α and Ω ..
查询是 Α 和 Ω ..
Lets say you pick http://site.tld/[:query]
.
让我们说你选择http://site.tld/[:query]
。
In that case you have one primary source of language: the content of [:query]
segment; and two additional sources:
在这种情况下,你有一个主要的语言来源:[:query]
段的内容;以及另外两个来源:
- value
$_COOKIE['lang']
for that particular browser - list of languages in HTTP Accept-Language (1), (2)header
First, you need to match the query to one of defined routing patterns (if your pick is Laravel, then read here). On successful match of pattern you then need to find the language.
首先,您需要将查询与定义的路由模式之一匹配(如果您选择的是 Laravel,请阅读此处)。成功匹配模式后,您需要找到语言。
You would have to go through all the segments of the pattern. Find the potential translations for all of those segments and determine which language was used. The two additional sources (cookie and header) would be used to resolve routing conflicts, when (not "if") they arise.
您必须遍历模式的所有部分。找到所有这些段的潜在翻译并确定使用的是哪种语言。两个额外的来源(cookie 和标头)将用于解决路由冲突,当(不是“如果”)它们出现时。
Take for example: http://site.tld/blog/novinka
.
举个例子:http://site.tld/blog/novinka
。
That's transliteration of "блог, новинка"
, that in English means approximately "blog", "latest"
.
这是 的音译"блог, новинка"
,在英语中的意思是大约"blog", "latest"
。
As you can already notice, in Russian "блог" will be transliterated as "blog". Which means that for the first part of [:query]
you (in the best case scenario) will end up with ['en', 'ru']
list of possible languages. Then you take next segment - "novinka". That might have only one language on the list of possibilities: ['ru']
.
正如您已经注意到的,在俄语中,“блог”将被音译为“博客”。这意味着对于[:query]
你的第一部分(在最好的情况下)最终会['en', 'ru']
得到可能的语言列表。然后你选择下一个部分 - “novinka”。在可能性列表中可能只有一种语言:['ru']
.
When the list has one item, you have successfully found the language.
当列表中有一项时,您已成功找到该语言。
But if you end up with 2 (example: Russian and Ukrainian) or more possibilities .. or 0 possibilities, as a case might be. You will have to use cookie and/or header to find the correct option.
但是,如果您最终得到 2(例如:俄语和乌克兰语)或更多可能性......或 0 种可能性,视情况而定。您将不得不使用 cookie 和/或标题来找到正确的选项。
And if all else fails, you pick the site's default language.
如果所有其他方法都失败了,您可以选择站点的默认语言。
Language as parameter
语言作为参数
The alternative is to use URL, that can be defined as http://site.tld/[:language]/[:query]
. In this case, when translating query, you do not need to guess the language, because at that point you already know which to use.
另一种方法是使用 URL,它可以定义为http://site.tld/[:language]/[:query]
. 在这种情况下,在翻译查询时,您无需猜测语言,因为那时您已经知道要使用哪种语言了。
There is also a secondary source of language: the cookie value. But here there is no point in messing with Accept-Language header, because you are not dealing with unknown amount of possible languages in case of "cold start" (when user first time opens site with custom query).
还有一个次要的语言来源:cookie 值。但是这里没有必要弄乱 Accept-Language 标头,因为在“冷启动”的情况下(当用户第一次使用自定义查询打开站点时),您没有处理未知数量的可能语言。
Instead you have 3 simple, prioritized options:
相反,您有 3 个简单的优先选项:
- if
[:language]
segment is set, use it - if
$_COOKIE['lang']
is set, use it - use default language
- 如果
[:language]
设置了段,则使用它 - 如果
$_COOKIE['lang']
设置,使用它 - 使用默认语言
When you have the language, you simply attempt to translate the query, and if translation fails, use the "default value" for that particular segment (based on routing results).
当您拥有该语言时,您只需尝试翻译查询,如果翻译失败,则使用该特定段的“默认值”(基于路由结果)。
Isn't here a third option?
这里不是第三种选择吗?
Yes, technically you can combine both approaches, but that would complicate the process and only accommodate people who want to manually change URL of http://site.tld/en/news
to http://site.tld/de/news
and expect the news page to change to German.
是的,从技术上讲,您可以将这两种方法结合起来,但这会使过程复杂化,并且仅适用于想要手动更改http://site.tld/en/news
to 的URLhttp://site.tld/de/news
并希望将新闻页面更改为德语的人。
But even this case could probable be mitigated using cookie value (which would contain information about previous choice of language), to implement with less magic and hope.
但即使是这种情况也可以使用 cookie 值(其中包含有关先前选择的语言的信息)来缓解,以更少的魔法和希望来实现。
Which approach to use?
使用哪种方法?
As you might already guessed, I would recommend http://site.tld/[:language]/[:query]
as the more sensible option.
正如您可能已经猜到的那样,我建议将其http://site.tld/[:language]/[:query]
作为更明智的选择。
Also in real word situation you would have 3rd major part in URL: "title". As in name of the product in online shop or headline of article in news site.
同样在实际情况下,您将在 URL 中拥有第三个主要部分:“标题”。如在线商店中的产品名称或新闻网站中的文章标题。
Example: http://site.tld/en/news/article/121415/EU-as-global-reserve-currency
例子: http://site.tld/en/news/article/121415/EU-as-global-reserve-currency
In this case '/news/article/121415'
would be the query, and the 'EU-as-global-reserve-currency'
is title. Purely for SEO purposes.
在这种情况下'/news/article/121415'
将是查询,'EU-as-global-reserve-currency'
是标题。纯粹出于 SEO 目的。
Can it be done in Laravel?
可以在 Laravel 中完成吗?
Kinda, but not by default.
有点,但不是默认。
I am not too familiar with it, but from what I have seen, Laravel uses simple pattern-based routing mechanism. To implement multilingual URLs you will probably have to extend core class(es), because multilingual routing need access to different forms of storage (database, cache and/or configuration files).
我对它不是太熟悉,但据我所知,Laravel 使用了简单的基于模式的路由机制。要实现多语言 URL,您可能必须扩展核心类,因为多语言路由需要访问不同形式的存储(数据库、缓存和/或配置文件)。
It's routed. What now?
路由了 现在怎么办?
As a result of all you would end up with two valuable pieces of information: current language and translated segments of query. These values then can be used to dispatch to the class(es) which will produce the result.
由于所有这些,您最终会得到两条有价值的信息:当前语言和已翻译的查询段。然后,这些值可用于分派到将产生结果的类。
Basically, the following URL: http://site.tld/ru/blog/novinka
(or the version without '/ru'
) gets turned into something like
基本上,以下 URL:(http://site.tld/ru/blog/novinka
或没有 的版本'/ru'
)变成类似
$parameters = [
'language' => 'ru',
'classname' => 'blog',
'method' => 'latest',
];
Which you just use for dispatching:
您仅用于分派:
$instance = new {$parameter['classname']};
$instance->{'get'.$parameters['method']}( $parameters );
.. or some variation of it, depending on the particular implementation.
.. 或者它的一些变体,取决于特定的实现。
回答by Glitch Desire
Implementing i18n Without The Performance Hit Using a Pre-Processor as suggested by Thomas Bley
按照 Thomas Bley 的建议,使用预处理器在不影响性能的情况下实现 i18n
At work, we recently went through implementation of i18n on a couple of our properties, and one of the things we kept struggling with was the performance hit of dealing with on-the-fly translation, then I discovered this great blog post by Thomas Bleywhich inspired the way we're using i18n to handle large traffic loads with minimal performance issues.
在工作中,我们最近在我们的几个属性上实现了 i18n,我们一直在努力解决的一件事是处理即时翻译的性能下降,然后我发现了 Thomas Bley 的这篇很棒的博客文章这启发了我们使用 i18n 以最小的性能问题处理大流量负载的方式。
Instead of calling functions for every translation operation, which as we know in PHP is expensive, we define our base files with placeholders, then use a pre-processor to cache those files (we store the file modification time to make sure we're serving the latest content at all times).
我们不是为每个翻译操作调用函数,因为我们知道在 PHP 中这是昂贵的,我们使用占位符定义我们的基本文件,然后使用预处理器来缓存这些文件(我们存储文件修改时间以确保我们正在服务随时提供最新内容)。
The Translation Tags
翻译标签
Thomas uses {tr}
and {/tr}
tags to define where translations start and end. Due to the fact that we're using TWIG, we don't want to use {
to avoid confusion so we use [%tr%]
and [%/tr%]
instead. Basically, this looks like this:
Thomas 使用{tr}
和{/tr}
标签来定义翻译的开始和结束位置。由于我们使用的是 TWIG,我们不想使用它{
来避免混淆,所以我们使用[%tr%]
and[%/tr%]
代替。基本上,这看起来像这样:
`return [%tr%]formatted_value[%/tr%];`
Note that Thomas suggests using the base English in the file. We don't do this because we don't want to have to modify all of the translation files if we change the value in English.
请注意,Thomas 建议在文件中使用基本英语。我们不这样做是因为如果我们更改英文值,我们不想修改所有翻译文件。
The INI Files
INI 文件
Then, we create an INI file for each language, in the format placeholder = translated
:
然后,我们为每种语言创建一个 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)
It would be trivial to allow a user to modify these inside the CMS, just get the keypairs by a preg_split
on \n
or =
and making the CMS able to write to the INI files.
允许用户在 CMS 内部修改这些是微不足道的,只需通过preg_split
on \n
or获取密钥对=
并使 CMS 能够写入 INI 文件。
The Pre-Processor Component
预处理器组件
Essentially, Thomas suggests using a just-in-time 'compiler' (though, in truth, it's a preprocessor) function like this to take your translation files and create static PHP files on disk. This way, we essentially cache our translated files instead of calling a translation function for every string in the file:
本质上,Thomas 建议使用像这样的即时“编译器”(尽管实际上它是一个预处理器)函数来获取翻译文件并在磁盘上创建静态 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;
}
Note: I didn't verify that the regex works, I didn't copy it from our company server, but you can see how the operation works.
注意:我没有验证正则表达式是否有效,我没有从我们公司的服务器复制它,但是您可以看到操作是如何工作的。
How to Call It
如何称呼它
Again, this example is from Thomas Bley, not from me:
同样,这个例子来自 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();
We store the language in a cookie (or session variable if we can't get a cookie) and then retrieve it on every request. You could combine this with an optional $_GET
parameter to override the language, but I don't suggest subdomain-per-language or page-per-language because it'll make it harder to see which pages are popular and will reduce the value of inbound links as you'll have them more scarcely spread.
我们将语言存储在 cookie(或会话变量,如果我们无法获取 cookie)中,然后在每次请求时检索它。您可以将其与可选$_GET
参数结合使用来覆盖语言,但我不建议使用 subdomain-per-language 或 page-per-language 因为这会使查看哪些页面更受欢迎并且会降低入站的价值链接,因为你会让它们更难传播。
Why use this method?
为什么要使用这种方法?
We like this method of preprocessing for three reasons:
我们喜欢这种预处理方法有以下三个原因:
- The huge performance gain from not calling a whole bunch of functions for content which rarely changes (with this system, 100k visitors in French will still only end up running translation replacement once).
- It doesn't add any load to our database, as it uses simple flat-files and is a pure-PHP solution.
- The ability to use PHP expressions within our translations.
- 不为很少改变的内容调用一大堆函数带来了巨大的性能提升(使用这个系统,10 万法语访问者仍然只会运行一次翻译替换)。
- 它不会给我们的数据库增加任何负载,因为它使用简单的平面文件并且是一个纯 PHP 解决方案。
- 在我们的翻译中使用 PHP 表达式的能力。
Getting Translated Database Content
获取翻译的数据库内容
We just add a column for content in our database called language
, then we use an accessor method for the LANG
constant which we defined earlier on, so our SQL calls (using ZF1, sadly) look like this:
我们只是在我们的数据库中为内容添加一个名为 的列language
,然后我们对LANG
我们之前定义的常量使用访问器方法,因此我们的 SQL 调用(使用 ZF1,遗憾的是)看起来像这样:
$query = select()->from($this->_name)
->where('language = ?', User::getLang())
->where('id = ?', $articleId)
->limit(1);
Our articles have a compound primary key over id
and language
so article 54
can exist in all languages. Our LANG
defaults to en_US
if not specified.
我们的文章有一个复合主键id
,language
因此文章54
可以存在于所有语言中。如果未指定,我们LANG
默认为en_US
。
URL Slug Translation
URL Slug 翻译
I'd combine two things here, one is a function in your bootstrap which accepts a $_GET
parameter for language and overrides the cookie variable, and another is routing which accepts multiple slugs. Then you can do something like this in your routing:
我在这里结合了两件事,一个是引导程序中的一个函数,它接受$_GET
语言参数并覆盖 cookie 变量,另一个是接受多个 slug 的路由。然后你可以在你的路由中做这样的事情:
"/wilkommen" => "/welcome/lang/de"
... etc ...
These could be stored in a flat file which could be easily written to from your admin panel. JSON or XML may provide a good structure for supporting them.
这些可以存储在可以从管理面板轻松写入的平面文件中。JSON 或 XML 可以提供一个很好的结构来支持它们。
Notes Regarding A Few Other Options
关于其他一些选项的注意事项
PHP-based On-The-Fly Translation
基于 PHP 的即时翻译
I can't see that these offer any advantage over pre-processed translations.
我看不出这些比预处理翻译有什么优势。
Front-end Based Translations
基于前端的翻译
I've long found these interesting, but there are a few caveats. For example, you have to make available to the user the entire list of phrases on your website that you plan to translate, this could be problematic if there are areas of the site you're keeping hidden or haven't allowed them access to.
我早就发现这些很有趣,但有一些警告。例如,您必须向用户提供您计划翻译的网站上的整个短语列表,如果您隐藏网站的某些区域或不允许他们访问,这可能会出现问题。
You'd also have to assume that all of your users are willing and able to use Javascript on your site, but from my statistics, around 2.5% of our users are running without it (or using Noscript to block our sites from using it).
您还必须假设您的所有用户都愿意并能够在您的网站上使用 Javascript,但根据我的统计,我们约有 2.5% 的用户在没有它的情况下运行(或使用 Noscript 阻止我们的网站使用它) .
Database-Driven Translations
数据库驱动的翻译
PHP's database connectivity speeds are nothing to write home about, and this adds to the already high overhead of calling a function on every phrase to translate. The performance & scalability issues seem overwhelming with this approach.
PHP 的数据库连接速度没有什么可写的,这增加了对每个要翻译的短语调用函数的高开销。这种方法的性能和可扩展性问题似乎势不可挡。
回答by Yaroslav
I suggest you not to invent a wheel and use gettext and ISO languages abbrevs list. Have you seen how i18n/l10n implemented in popular CMSes or frameworks?
我建议您不要发明轮子并使用 gettext 和 ISO 语言缩写列表。你有没有看到 i18n/l10n 在流行的 CMS 或框架中是如何实现的?
Using gettext you will have a powerful tool where many of cases is already implemented like plural forms of numbers. In english you have only 2 options: singular and plural. But in Russian for example there are 3 forms and its not as simple as in english.
使用 gettext 您将拥有一个强大的工具,其中许多情况已经像复数形式的数字一样实现。在英语中,您只有 2 个选项:单数和复数。但例如在俄语中有 3 种形式,它不像英语那么简单。
Also many translators already have experience to work with gettext.
此外,许多翻译人员已经有使用 gettext 的经验。
Take a look to CakePHPor Drupal. Both multilingual enabled. CakePHP as example of interface localization and Drupal as example of content translation.
看看CakePHP或Drupal。都启用了多语言。CakePHP 作为界面本地化的例子,Drupal 作为内容翻译的例子。
For l10n using database isn't the case at all. It will be tons on queries. Standard approach is to get all l10n data in memory in early stage (or during first call to i10n function if you prefer lazy loading). It can be reading from .po file or from DB all data at once. And than just read requested strings from array.
对于使用数据库的 l10n 根本不是这种情况。这将是大量的查询。标准方法是在早期获取内存中的所有 l10n 数据(如果您喜欢延迟加载,或者在第一次调用 i10n 函数期间)。它可以一次从 .po 文件或 DB 中读取所有数据。而不仅仅是从数组中读取请求的字符串。
If you need to implement online tool to translate interface you can have all that data in DB but than still save all data to file to work with it. To reduce amount of data in memory you can split all your translated messages/strings into groups and than load only that groups you need if it will be possible.
如果您需要实现在线工具来翻译界面,您可以将所有数据保存在数据库中,但仍将所有数据保存到文件中以使用它。为了减少内存中的数据量,您可以将所有翻译过的消息/字符串分成组,如果可能的话,只加载您需要的组。
So you totally right in your #3. With one exception: usually it is one big file not a per-controller file or so. Because it is best for performance to open one file. You probably know that some highloaded web apps compiles all PHP code in one file to avoid file operations when include/require called.
所以你在你的#3 中完全正确。除了一个例外:通常它是一个大文件,而不是每个控制器的文件。因为打开一个文件最有利于性能。您可能知道一些高负载的 Web 应用程序将所有 PHP 代码编译到一个文件中,以避免在调用 include/require 时进行文件操作。
About URLs. Google indirectly suggestto use translation:
关于网址。谷歌间接建议使用翻译:
to clearly indicate French content: http://example.ca/fr/vélo-de-montagne.html
清楚地表明法语内容:http: //example.ca/fr/vélo-de-montagne.html
Also i think you need to redirect user to default language prefix e.g. http://examlpe.com/about-uswill redirects to http://examlpe.com/en/about-usBut if your site use only one language so you don't need prefixes at all.
此外,我认为您需要将用户重定向到默认语言前缀,例如http://examlpe.com/about-us将重定向到http://examlpe.com/en/about-us但是如果您的网站只使用一种语言,那么您根本不需要前缀。
Check out: http://www.audiomicro.com/trailer-hit-impact-psychodrama-sound-effects-836925http://nl.audiomicro.com/aanhangwagen-hit-effect-psychodrama-geluidseffecten-836925http://de.audiomicro.com/anhanger-hit-auswirkungen-psychodrama-sound-effekte-836925
退房: http://www.audiomicro.com/trailer-hit-impact-psychodrama-sound-effects-836925 http://nl.audiomicro.com/aanhangwagen-hit-effect-psychodrama-geluidseffecten-836925 HTTP:/ /de.audiomicro.com/anhanger-hit-auswirkungen-psychodrama-sound-effekte-836925
Translating content is more difficult task. I think it will be some differences with different types of content e.g. articles, menu items etc. But in #4 you're in the right way. Take a look in Drupal to have more ideas. It have clear enough DB schema and good enough interface for translating. Like you creating article and select language for it. And than you can later translate it to other languages.
翻译内容是更困难的任务。我认为这会与不同类型的内容有所不同,例如文章、菜单项等。但在 #4 中,您的方法是正确的。看看 Drupal 以获得更多想法。它有足够清晰的数据库架构和足够好的翻译界面。就像您创建文章并为其选择语言一样。然后您可以稍后将其翻译成其他语言。
I think it isn't problem with URL slugs. You can just create separate table for slugs and it will be right decision. Also using right indexes it isn't problem to query table even with huge amount of data. And it wasn't full text search but string match if will use varchar data type for slug and you can have an index on that field too.
我认为这不是 URL slug 的问题。您可以为 slug 创建单独的表,这将是正确的决定。同样使用正确的索引,即使有大量数据查询表也不是问题。它不是全文搜索,而是字符串匹配,如果将使用 varchar 数据类型作为 slug 并且您也可以在该字段上建立索引。
PS Sorry, my English is far from perfect though.
PS 对不起,我的英语远非完美。
回答by user3749746
It depends on how much content your website has. At first I used a database like all other people here, but it can be time-consuming to script all the workings of a database. I don't say that this is an ideal method and especially if you have a lot of text, but if you want to do it fast without using a database, this method could work, though, you can't allow users to input data which will be used as translation-files. But if you add the translations yourself, it will work:
这取决于您的网站有多少内容。起初我和这里的其他人一样使用了一个数据库,但是编写数据库的所有工作脚本可能很耗时。我并不是说这是一种理想的方法,尤其是当您有大量文本时,但是如果您想在不使用数据库的情况下快速完成,这种方法可以工作,但是,您不能允许用户输入数据将用作翻译文件。但是如果你自己添加翻译,它会起作用:
Let's say you have this text:
假设您有以下文本:
Welcome!
You can input this in a database with translations, but you can also do this:
您可以将其输入到带有翻译的数据库中,但您也可以这样做:
$welcome = array(
"English"=>"Welcome!",
"German"=>"Willkommen!",
"French"=>"Bienvenue!",
"Turkish"=>"Ho?geldiniz!",
"Russian"=>"Добро пожаловать!",
"Dutch"=>"Welkom!",
"Swedish"=>"V?lkommen!",
"Basque"=>"Ongietorri!",
"Spanish"=>"Bienvenito!"
"Welsh"=>"Croeso!");
Now, if your website uses a cookie, you have this for example:
现在,如果您的网站使用 cookie,您可以使用以下示例:
$_COOKIE['language'];
To make it easy let's transform it in a code which can easily be used:
为了方便起见,让我们将其转换为易于使用的代码:
$language=$_COOKIE['language'];
If your cookie language is Welsh and you have this piece of code:
如果您的 cookie 语言是威尔士语并且您有以下代码:
echo $welcome[$language];
The result of this will be:
这样做的结果将是:
Croeso!
If you need to add a lot of translations for your website and a database is too consuming, using an array can be an ideal solution.
如果您需要为您的网站添加大量翻译并且数据库过于消耗,使用数组可能是一个理想的解决方案。
回答by Shushant
I will suggest you not to really depend of database for translation it could be really a messy task and could be a extreme problem in case of data encoding.
我建议您不要真正依赖数据库进行翻译,这可能是一项非常麻烦的任务,并且在数据编码的情况下可能是一个极端问题。
I had face similar issue while ago and written following class to solve my problem
我之前遇到过类似的问题,并写了以下课程来解决我的问题
Object: 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;
}
}
Usage
用法
<?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');
How it works
这个怎么运作
{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')
{a:1}
替换为传递给方法的第一个参数 替换为传递给方法Locale::translate('key_name','arg1')
{a:2}
的第二个参数Locale::translate('key_name','arg1','arg2')
How detection works
检测的工作原理
- By default if
geoip
is installed then it will return country code bygeoip_country_code_by_name
and if geoip is not installed the fallback toHTTP_ACCEPT_LANGUAGE
header
- 默认情况下,如果
geoip
已安装,则它将返回国家代码geoip_country_code_by_name
,如果未安装 geoip,则回退到HTTP_ACCEPT_LANGUAGE
标头
回答by Remy
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. Why? Cause the url is essential for SEO.
只是一个子答案:绝对使用前面带有语言标识符的翻译网址:http: //www.domain.com/nl/over-ons
Hybride 解决方案往往会变得复杂,所以我会坚持下去。为什么?因为网址对于搜索引擎优化至关重要。
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!
但一般来说,为什么不使用 Drupal 呢?我知道每个人都想建立自己的 CMS,因为它更快、更精简等等。但这真的是一个坏主意!
回答by Laurynas Mali?auskas
I had the same probem a while ago, before starting using Symfonyframework.
不久前,在开始使用Symfony框架之前,我有同样的问题。
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.
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.
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.
i think i answered that in #2
your idea is perfectly logical. this one is pretty simple and i think will not make you any problems.
只需使用具有参数 pageId(或 objectId、#2 中描述的 objectTable)、目标语言和后备(默认)语言的可选参数的函数 __()。可以在某些全局配置中设置默认语言,以便以后更轻松地更改它。
为了将内容存储在数据库中,我使用了以下结构:(pageId、语言、内容、变量)。
pageId 将是您要翻译的页面的 FK。如果您有其他对象,例如新闻、画廊或其他任何对象,只需将其拆分为 2 个字段 objectId、objectTable。
语言 - 显然它会存储 ISO 语言字符串 EN_en、LT_lt、EN_us 等。
内容 - 要与用于变量替换的通配符一起翻译的文本。示例“您好,%%name%% 先生。您的帐户余额为 %%balance%%。”
variables - json 编码的变量。PHP 提供了快速解析这些的函数。示例“名称:Laurynas,余额:15.23”。
你也提到了弹头场。您可以随意将其添加到此表中,以便快速搜索它。
您的数据库调用必须通过缓存翻译减少到最低限度。它必须存储在 PHP 数组中,因为它是 PHP 语言中最快的结构。您将如何进行这种缓存取决于您。根据我的经验,您应该为每种支持的语言设置一个文件夹,并为每个 pageId 设置一个数组。更新翻译后应重建缓存。只应重新生成更改后的数组。
我想我在 #2 中回答了这个问题
你的想法是完全合乎逻辑的。这个很简单,我认为不会给你带来任何问题。
URLs should be translated using the stored slugs in the translation table.
应使用翻译表中存储的 slug 来翻译 URL。
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.
看看Symfony 翻译组件。这对你来说可能是一个很好的代码库。
回答by JG Estiot
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.
我不会试图完善已经给出的答案。相反,我将告诉您我自己的 OOP PHP 框架处理翻译的方式。
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.
在内部,我的框架使用 en、fr、es、cn 等代码。一个数组保存了网站支持的语言: array('en','fr','es','cn') 语言代码通过 $_GET (lang=fr) 传递,如果不传递或无效,它设置为数组中的第一种语言。所以在程序执行过程中的任何时候,从一开始,当前的语言都是已知的。
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
1)来自类(或程序代码)的错误消息 2)来自类(或程序代码)的非错误消息 3)页面内容(通常存储在数据库中) 4)站点范围的字符串(如网站名称) 5)脚本 -特定字符串
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
第二种错误消息更像是表单验证出错时收到的消息。(“您不能将......留空”或“请选择一个超过 5 个字符的密码”)。字符串需要在类运行之前加载。我知道是什么
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.
对于实际的页面内容,我使用一种语言的表格,每个表格都以语言代码为前缀。所以en_content是英文内容的表,es_content是西班牙的,cn_content是china的,fr_content是法语的。
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 ;)
第四种字符串与整个网站相关。这是通过使用语言代码命名的配置文件加载的,即 en_lang.php、es_lang.php 等。在全局语言文件中,您需要在英文全局文件中加载已翻译的语言,例如 array('English','Chinese', 'Spanish','French') 和 array('Anglais','Chinois', ' Espagnol', 'Francais') 在法语文件中。因此,当您为语言选择填充下拉菜单时,它使用的是正确的语言;)
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 "Hyman'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.
在我的应用周期中,首先加载全局语言文件。在那里,您不仅会找到全局字符串(如“Hyman 的网站”),还会找到某些类的设置。基本上任何依赖于语言或文化的东西。其中的一些字符串包括日期掩码(MMDDYYYY 或 DDMMYYYY)或 ISO 语言代码。在主语言文件中,我包含了各个类的字符串,因为它们太少了。
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.
从磁盘读取的第二个也是最后一个语言文件是脚本语言文件。lang_en_home_welcome.php 是 home/welcome 脚本的语言文件。脚本由模式(home)和动作(welcome)定义。每个脚本都有自己的文件夹,其中包含 config 和 lang 文件。
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.
只是对翻译字符串的实现方式做一个简短的总结。我的框架有一个全局变量 $manager,它运行对任何其他服务可用的服务。例如,表单服务获取 html 服务并使用它来编写 html。我系统上的一项服务是翻译服务。$translator->set($service,$code,$string) 设置当前语言的字符串。语言文件是此类语句的列表。$translator->get($service,$code) 检索翻译字符串。$code 可以是像 1 这样的数字或像“no_connection”这样的字符串。服务之间不会发生冲突,因为每个服务在翻译器的数据区中都有自己的命名空间。
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.
我把这个贴在这里是希望它能像我几年前那样免去重新发明轮子的任务。
回答by Dr. Dama
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
我建议看看高级 CMS
Typo3
for PHP
(I know there is a lot of stuff but thats the one I think is most mature)
Typo3
因为PHP
(我知道有很多东西,但那是我认为最成熟的)
Plone
in Python
Plone
在 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.
如果您发现 2013 年的网络应该有所不同,那么请从头开始。这意味着组建一支由高技能/经验丰富的人员组成的团队来构建新的 CMS。可能您想为此目的查看聚合物。
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.
如果涉及编码和多语言网站/母语支持,我认为每个程序员都应该对 unicode 有所了解。如果您不知道 unicode,您肯定会弄乱您的数据。不要使用成千上万的 ISO 代码。它们只会为您节省一些内存。但是你可以用 UTF-8 做任何事情,甚至可以存储中文字符。但是为此,您需要存储 2 个或 4 个字节的字符,使其基本上是 utf-16 或 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. e.g. a Domain could be very similar like:
如果是关于 URL 编码,那么您不应该混合编码,并且至少对于域名,存在由提供浏览器等应用程序的不同大厅定义的规则。例如,域可能非常相似,例如:
ьankofamerica.com or bankofamerica.com samesamebutdifferent ;)
ьankofamerica.com 或 bankofamerica.com samesamebutdifferent ;)
Of course you need the filesystem to work with all encodings. Another plus for unicode using utf-8 filesystem.
当然,您需要文件系统来处理所有编码。使用 utf-8 文件系统的 unicode 的另一个优点。
If its about translations, think about the structure of documents. e.g. 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.
如果是关于翻译,请考虑文档的结构。例如一本书或一篇文章。您拥有docbook
了解这些结构的规范。但在 HTML 中,它只是关于内容块。因此,您希望在该级别、网页级别或域级别进行翻译。因此,如果一个块不存在,它只是不存在,如果一个网页不存在,您将被重定向到上层导航级别。如果一个域的导航结构应该完全不同,那么......它是一个完全不同的结构来管理。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.
如果它是关于框架的,我所知道的最成熟的框架,做像 MVC 这样的一般东西(流行语我真的很讨厌它!比如“性能”如果你想卖东西,用性能和功能丰富的词,你卖......什么地狱)是Zend
。事实证明,为 php 混沌编码人员带来标准是一件好事。但是,typo3 除了 CMS 之外还有一个框架。最近重新开发,现在叫flow3。这些框架当然涵盖了数据库抽象、模板和缓存概念,但具有各自的优势。
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, hashbangsetc.
如果是关于缓存……那可能会非常复杂/多层次。在 PHP 中,您会想到加速器、操作码,还有 html、httpd、mysql、xml、css、js ……任何类型的缓存。当然,某些部分应该被缓存,而像博客答案这样的动态部分不应该被缓存。有些应该使用生成的 url 通过 AJAX 请求。JSON、hashbangs等。
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.
你也想做出统计,或许已经分发系统/ Facebook的等任何软件的Facebook上建造你的洁癖CMS之上......所以你需要不同类型的数据库inmemory,bigdata,XML,任何.
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.
好吧,我认为现在就足够了。如果您还没有听说过typ3 / plone 或提到的框架,那么您有足够的时间去学习。在这条路上,你会发现很多你还没有问过的问题的解决方案。
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.
如果那么您认为,让我们创建一个新的 CMS,因为它的 2013 年和 php 无论如何都将消亡,那么欢迎您加入任何其他开发人员组,希望不会迷路。
Good luck!
祝你好运!
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)
顺便说一句。未来人们将不再拥有任何网站呢?我们都会在 google+ 上吗?我希望开发者变得更有创意,做一些有用的事情(不要被 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
如果您有一个 php mysql CMS 并且您想嵌入多语言支持。您可以将表与任何语言的附加列一起使用,或者在同一个表中插入带有对象 ID 和语言 ID 的翻译,或者为任何语言创建一个相同的表并在那里插入对象,然后根据需要进行选择联合将它们全部显示出来。对于数据库使用 utf8 一般 ci,当然在前端/后端使用 utf8 文本/编码。我已经按照您已经解释的方式使用了 url 路径段作为 url
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 e.g.
domain.org/en/about 您可以将 lang ID 映射到您的内容表。无论如何,您需要为您的 url 设置一个参数映射,因此您想定义一个从 URL 中的路径段映射的参数,例如
domain.org/en/about/employees/IT/administrators/
domain.org/en/about/employees/IT/administrators/
lookup configuration
查找配置
pageid| url
页面ID| 网址
1 | /about/employees/../..
1 | /关于/员工/../..
1 | /../about/employees../../
1 | /../关于/员工../../
map parameters to url pathsegment ""
将参数映射到 url 路径段 ""
$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
并且不要忘记,您需要将url“重写”到您生成的php文件中,在大多数情况下是index.php
回答by user3445130
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 incremented)
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,
当用户从下拉列表或任何区域中选择任何语言时,然后在会话中保存所选语言 ID,例如,
$_SESSION['language']=1;
$_SESSION['language']=1;
Now fetch data from database table ‘content' based on language id stored in session.
现在根据会话中存储的语言 ID 从数据库表“内容”中获取数据。
Detail may found here http://skillrow.com/multilingual-website-in-php-2/
详细信息可以在这里找到 http://skillrow.com/multilingual-website-in-php-2/