如何编写多语言网站
如何扩大网站以支持多种语言。
引言
您是否创建了一个网站并希望将其推广到全球并添加多语言功能?您是否在思考如何开发一个包含多种语言的网站?如果是,那么这篇文章适合您。我将讨论编写包含多种语言的网站的几种选择。我将逐一介绍每种选项的优缺点,同时关注易用性、编程便捷性和性能影响。背景
本指南由我撰写,我是一名经验丰富的传统 ASP(Active Server Pages)程序员。我相信这篇文章即使对于 ASP.Net 程序员来说也会有所收获,因为他们在开发大型项目时面临着相同的根本性问题。您应该了解您的代码,才能理解为其添加多语言功能所带来的影响;我通常使用 Microsoft® Notepad® 编写我的大型和小型网站项目,所以我对我的代码非常了解。您也应该这样!使用代码
我没有提供完整的源代码存档。我提供的是关于如何准备多语言支持的总体思路。我下面写的代码展示了一个通用的想法,不应该被直接复制到您自己的网站中;请仔细思考,如果您被说服了,再将其应用到您的网站上。主文章
全球扩张与多语言支持问题
我曾有幸于 1999 年在以色列的 Bono Pie LTD 担任首席程序员。当时我才 19 岁;由于那是 Web 服务发展的初期,我在大型 Web 开发领域经验不足,几乎和该领域的所有人一样。然而,也许出乎您的意料,Bono Pie LTD 取得了持久的成功,约有 20 名员工,其中约有四名是我团队的程序员。该网站使用 VBScript 编写的 ASP 进行编程,并包含大量炫目的 HTML、CSS、Flash® 文件和其他装饰。我们的数据库是 Microsoft® SQL® Server,在从 Microsoft® Access® 迁移之后,花费了大量 DBA 时间,其中很多时间是我自己的。我们每天努力工作以保持业务的步调,创建新功能并修复 bug。
预见到很快就需要支持其他语言,我们开始着手迁移我们的网站。这并非易事,因为我们网站的语言是以色列语(希伯来语),其字母是从右到左(RTL)书写的。我将省略关于如何同时实现支持 LTR(从左到右)和 RTL 的详细描述,因为我认为你们大多数人不会遇到这种情况。因此,我们的问题简化为“我如何创建支持不同语言的网站页面?”
首先,让我们看看应该怎么做。我网站的代码片段可能如下所示:
<table border=0>
<tr width=100%>
<td width=100%>
Friends? Friends?!?
We've only gone out together three times,
and already you're telling me you want to be friends?
</td>
</tr>
</table>
为了支持多语言,我必须以某种方式集成同一种文本的多种语言。一个有些奇特的解决方案可能是:<table border=0>
<tr width=100%>
<td width=100%>
<% if MyLanguage="English" then%>
Friends? Friends?!?
We've only gone out together three times,
and already you're telling me you want to be friends?
<% elseif MyLanguage="Spanish" then%>
¿Amigos? ¿Amigos?!?
¿Hemos salido solamente juntos tres veces,
y ya me estás diciendo que quieres ser amigos?
<% end if%>
</td>
</tr>
</table>
然而,在这里嵌入文本会增加普通翻译人员翻译网站的难度;我必须与他们一起工作,确保他们不会破坏我的代码;这是一个容易出错的解决方案。相反,我希望有一种设计,能够让我将所有需要翻译的文本集中到一个地方。此外,很容易看出必须有人工参与,因为有些部分需要翻译(文本),而另一些部分(HTML 或 ASP 部分)则不需要翻译。我知道我必须在保持我随意更改代码的自由度的同时添加这个功能。因此,诸如复制网站并为新语言手动更改每个副本之类的解决方案是灾难性的,因为我将无法再维护我的代码。所以,我们知道我们只需要一个代码源。但是,如果我们只创建一个代码源,它将如何转换为多种语言呢?一个简单但有些错误的解决方案是将特定于语言的数据嵌入到 ASP 指令中,最典型的是作为带有唯一 ID 来标识特定字符串的函数调用;这种方法对于新时代的开发者来说很简单,因为它类似于字符串表。例如:
<table border=0>
<tr width=100%>
<td width=100%>
<%= GetLinguisticText("FamousQuotationPage_TitleHeader")%>
</td>
</tr>
</table>
尽管此机制表面上类似于我的最终解决方案,但实际上截然不同。上述方法的缺点是并非所有特定于语言的内容都可以包含此类指令;我们也无法将其添加到 HTML 页面以及图像中。如果我们向 HTML 文件中添加此类命令,它将显示给用户,而无助于我们实现多语言支持。支持 HTML 文件的另一种方法是将语言物理地分割成多个 HTML 文件;然而,这种方法倾向于与我的第一个建议相关;此外,它带来了新的障碍,因为代码现在必须考虑用于选择正确 HTML 文件的语言。所有这些都同样适用于图像文件(PNG、JPG、GIF 等)。有人可能会说一个令人满意的解决方案是简单地将 HTML 文件更改为 ASP 文件;然而,这种情况会对性能和网站结构产生影响。所以,我们需要一个更通用的答案。为了支持所有文件类型的文本而不影响网站,必须采取不同的方法。与其将文本指令集成到文件中,不如将文本指令放在一个文件中,此后称为“代码”,将包含文本的文件放在另一个文件中,此后称为“结果”;即代码包含文本的标记,而结果文件包含每种语言的文本。为了实现这一点,我们创建了一个小的 ASP 网站,此后称为“引擎”,它动态生成结果文件。这样,当我们想更改网站的任何内容时,我们只需要编辑代码即可轻松生成结果文件。当然,我们的解决方案不像上述解决方案那样自动化;但是,它支持所有文本文件类型,甚至被扩展到支持其他文件类型,例如图像和 Macromedia® Flash® 文件。
提出的解决方案
首先,我们需要在代码页面中放置一个特定的标记,以便我们的引擎知道在哪里放置翻译后的文本。一个很好的记法是这样的:<table border=0>
<tr width=100%>
<td width=100%>
<GetLinguisticText ID="FamousQuotationPage_TitleHeader" />
</td>
</tr>
</table>
在代码文件中使用这样的记法将允许其他属性使用未来的选项。我们的引擎将查找所有代码页面中的 `GetLinguisticText` 标签,并在构建结果文件时将其替换为适当的文本。一个简单的引擎代码可以是:''' This function read the content from <CodeFileName>,
''' Translate it by replacing all '<GetLinguisticText ID="XXX" />'
''' with the text indicated in the <ResultLanguage> String Table.
''' Then, it places it in a corresponding file in the <ResultLanguage> folder.
Sub GenerateResultFileFromCodeFile(CodeFileName, ResultLanguage)
Dim FSO
Set FSO=Server.CreateObject("Scripting.FileSystemObject")
'''' Reading the code page into CodeText
Dim CodeText
Dim CodeFile
' Open the file for reading
Set CodeFile=FSO.OpenTextFile(GetCodeDir() + CodeFileName, 1)
CodeText=CodeFile.ReadAll
CodeFile.Close
Set CodeFile=Nothing
'''' Translating CodeText into ResultText
'''' by replacing each tag with its corresponding text
Dim ResultText
Dim IndexOfTag
Dim TagStart
TagStart="<GetLinguisticText "
Dim TagEnd
TagEnd=" />"
IndexOfTag=InStr(CodeText,TagStart)
While IndexOfTag>0
' Copy text before the appearance of the tag
If IndexOfTag>1 Then
' Copying all characters before the tag into the result
ResultText=ResultText+Left(CodeText, IndexOfTag-1)
' removing the copied part
CodeText=Mid(CodeText,IndexOfTag)
End If
' Trimming the tag's header
CodeText=Mid(CodeText,Len(TagStart)+1)
'''' Trimming the ID attribute
'''' Should leave CodeText without the tag at all,
'''' while appending the text into ResultText.
' The attribute of ID is: ID="XXX"
IdAttributeStart="ID="""
IdAttributeEnd=""""
'''' Could not parse the ID attribute
If Left(CodeText, Len(IdAttributeStart))<>IdAttributeStart Then
'Raise a user-defined error
Err.Raise 8
Err.Description = "Invalid input encountered while parsing
code page to translate (" + CodeFileName + ")"
Err.Source = "Translator"
End If
' Trimming the attribute's header
CodeText=Mid(CodeText,Len(IdAttributeStart)+1)
' Locating the end of the attribute
Dim IdAttributeEndIndex
IdAttributeEndIndex=InStr(CodeText, IdAttributeEnd)
Dim StringID
' Found the value of the ID tag
StringID=Left(CodeText, IdAttributeEndIndex-1)
' Trimming the attribute's footer
CodeText=Mid(CodeText, IdAttributeEndIndex-1+Len(IdAttributeEnd)+1)
' Trimming the tag's footer
CodeText=Mid(CodeText, Len(TagEnd)+1)
'''' Translating StringID into its text
ResultText=ResultText+GetLinguisticText(StringID, ResultLanguage)
' looking for the next tag in the same file
IndexOfTag=InStr(CodeText,TagStart)
Wend
' Copying the rest of the Code into the Result
ResultText=ResultText+CodeText
'''' Writing the translated code page into the result file
Dim ResultFile
' Open the file for writing and create the file if needed.
Set ResultFile=FSO.OpenTextFile(GetResultDirForLanguage(ResultLanguage)
+ CodeFileName, 2, True, -2)
ResultFile.Write ResultText
ResultFile.Close
Set ResultFile=Nothing
Set FSO=Nothing
End Sub
我花了大约半个小时在 Notepad® 中编写了这段代码,并用大约一分半钟的时间进行了调试,迭代了三次。现在您需要做的就是创建三个缺失的函数:`Function GetCodeDir`、`Function GetResultDirForLanguage(Language)` 和 `Function GetLinguisticText(StringID, ResultLanguage)`;后者可能应该放在数据库中的字符串表中。现在您可以使用新机制实际重建您的网站。这就是繁琐之处:您必须手动将所有文本插入到其指定的字符串表中,并用 `GetLinguisticText` 标签及其相应的 ID 替换它们。
完成劳动后,您可以通过调用 `GenerateResultFileFromCodeFile` 来重新生成您所有的页面。您可能可以使用一个表来列出您的所有页面。另一个存储所有语言的表也会很合适。我还创建了一个表来列出所有 TextID,以及每个语言的一个表用于翻译,其中包含键值对。随时表达您的想法。
简化翻译过程
当然,您可以要求翻译人员直接在数据库中翻译文本,但不要对他/她抱太大期望。如果您能够显示来自另一种语言或多种语言(如果可能)的文本,您可能会做得更好。我记得我创建了一个带有 ID、希伯来语文本和目标语言的表单;翻译人员只能更改正在翻译的语言的内容;还提供了上一页和下一页按钮,以便滚动不同的 ID。因此,要将网站翻译成一种新语言,例如在我当时的例子中的日语,翻译人员将使用一个单击例程翻译大量的文本。在网站上显示的一些文本实际上在其代码形式中是零散的,例如:
Hello <%= UserName%>, thank you for coming.
使用到目前为止的翻译机制,程序员将不得不这样做:<GenerateResultFileFromCodeFile ID="MainPage_GreetingBeforeUserName" />
<%= UserName%><GenerateResultFileFromCodeFile ID="MainPage_GreetingAfterUserName" />
MainPage_GreetingBeforeUserName::English = "Hello "
MainPage_GreetingBeforeUserName::Spanish = "Hola "
MainPage_GreetingAfterUserName::English = ", thank you for coming."
MainPage_GreetingAfterUserName::Spanish = ", gracias por venire."
虽然这看起来相当不错,但对此的翻译往往脱离了上下文,并且在从一种语言翻译到另一种语言时容易出错。由于我们的引擎会创建其他页面,甚至是 ASP 页面,我们甚至可以将小的 ASP 代码放入翻译单元中。即:<GenerateResultFileFromCodeFile ID="MainPage_Greeting" />
MainPage_Greeting::English = "Hello <%= UserName%>, thank you for coming."
MainPage_Greeting::Spanish = "Hola <%= UserName%>, gracias por venire."
然而,如果代码相当长并且包含许多 ASP 指令,建议不要包含它。但是,如果您愿意,可以添加符号:笑脸、商标;HTML 代码:`<br>`、`<hr>;等等,如果它们包含在文本中并且易于理解。即便如此,我做的一个很好的补充是将每个 TextID 与其页面关联起来,并向翻译人员预览该页面;即当翻译人员翻译给定的 TextID 时,他在屏幕的一半中会看到包含正在翻译的文本的页面;这减轻了上下文问题。此外,将特定页面的所有内容分组是有效的,因为它加快了翻译时间。我模糊地记得我们到底对其他文件(主要是图像文件)做了什么。我记得我们的网站有一个原型图像,使用了我们的母语希伯来语,翻译人员必须上传他们自己语言的相同图像。我甚至记得我们将源图像(例如 Photoshop® PSD 文件)放在同一个后台管理网站上;因此,添加一个包含图像源文件以及所有不同语言的图片名称的图像表就足够了。
再见。