`
shingo7
  • 浏览: 19353 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

网站实时简繁转换解决方案

阅读更多

    最近由于工作需要做了一个将网站转成繁体呈现给用户的功能,后来参看了许多成功案例,发现思路大体都是相同的,我现在将我具体的实现拿出来讨论一下,因为它不可能是一个百分百覆盖的功能,所以希望大家能多提意见。
很多网站都提供简体版和繁体版供用户浏览,有两种实现方式,一种是做两套不同版本的挂着,这种方式没什么技术性可言,暂不讨论了;我想讨论的是另一种实现方式,实时转换,即是只需硬做一套简体版的,繁体的可以通过实习转换来实现。
我的灵感来自于一个国外人做的网站,http://www.ponyfish.com
它可以说是一个工具,几乎可以为任何网站建立RSS源,这个工具从Step1到Step2在我看来是最难解决的,也可以说是最难覆盖所有网站的,它其实对目标网页的代码进行修改和剔除,然后重新拼接,还原网页原貌。由于Ponyfish这个工具是用来为网站的文章,新闻等建RSS源的,它并不关心页面上诸如JS,FLASH等元素,它剔除了所有的JS代码,所以,比如有某个网页的顶端导航和底端是引的JS,那在Ponyfish对网页还原后,用户看不到头和尾,只能看到内容的部分。
后来我也做了一个跟Ponyfish一模一样的工具,界面也是取的它的,只是出现英文的地方被我替换成中文了,原作者用php,我用的java。当时本想将Ponyfish的实现方法分享出来的,后来做了简繁转换后发现比Ponyfish复杂得多,Ponyfish关注的是网站的文章列表或新闻列表,简繁转换需要转换后的网站跟简体版一样,用户可以提交表单、搜索、用户体验,具有所有功能,必须完全可用。简繁转换的实现已经含盖了Ponyfish的核心功能,只不过Ponyfish多一些JS体验,多一些算法(如字符串求公共子串算法)。
由于需要对目标网页的代码进行操作,要对每个不同的HTML标签进行不同操作,所以我们必须能够读取遍历目标网站的结点。我使用的是HtmlParser,大致思路是将取得目标网页的HTML代码,遍历所有结点,对每个结点进行不同操作(即修改HTML代码),将最终得到的HTML代码推送到自己的空白页面显示,这样下来,只需循环一次,拼接成的HTML可以完全还原原貌。
具体操作如下:
假如我需要转换的网站是 http://tieba.baidu.com
我会将网址做为参数传到我的方法里,类似如下

http://www.mydomain.com?methodname=big5&url=http://tieba.baidu.com

 

在实际操作中,在样的链接出现在浏览器地址栏会对以后的转换造成很多不便,这些不便在下面会我提及,得做Urlrewirte

<rule>
	<from>^/big5/(.*)</from>
	<to>/big.do?methodname=big5&amp;url=$1</to>
</rule>

 最终我们URL地址会变成

http://www.mydomain.com/big5/http://tieba.baidu.com

 

当确定目标网站后,使用htmlparser遍历它的所有结节,即所有node

URL url = new URL(strurl);
HttpURLConnection httpUrl = (HttpURLConnection) url.openConnection();
Node node;
String contentType = httpUrl.getContentType();
String charSet = getCharset(contentType);
Lexer lexer = null;
if (charSet == null)
	charset = "GBK";
else
	charset = charSet;
lexer = new Lexer(new Page(new UnicodeInputStream(httpUrl
				.getInputStream(), charset), charset));

PrototypicalNodeFactory factory = new PrototypicalNodeFactory();
lexer.setNodeFactory(factory);
while (null != (node = lexer.nextNode())) {
	if((node instanceof RemarkNode))
		{…}
	else if((node instanceof TextNode))
		{…}
	else if((node instanceof LinkTag))
		{…}
	 ……
}

 大体按这样的流程走就可以了。
由于HTML代码的写法并没有一个明确的规范标准,所以怎样兼容尽可能多的写法是一个很大的问题,还会有很多转码的问题,因为网站上有的表单,引的JS,导的iframe,可能都是不一样的编码。根据我整合三个大的门户网的经历我将一些需要注意的问题罗列出来。

 

 

1:设置读取目标网站的编码
读取网站HTML代码时,得先设置好以什么样的编码来读取它,以上的代码我在遍历结点前先取了一次编码。

String charSet = getCharset(contentType);

 如果此时的charSet为空,我会为它设置一个默认值“GBK”,默认用GBK来遍历目标网站,当遍历到某个叫”Meta”的结点时,我会查找它有没有属性中包含类似content="text/html; charset=utf-8"的东西,如果没有,继续遍历,如果有,取得该编码值,将它与当前遍历网站的编码进行比较,如果一致则继续遍历,如果不一致,说明从一开始遍历时设置的编码就是错误的,这个时候需要使用新的编码重新遍历。
关于getCharset的方法,网上已有前人写出。

public String getCharset(String content) {
		final String CHARSET_STRING = "charset";
		int index;
		String ret;
		ret = null;
		if (null != content) {
			index = content.indexOf(CHARSET_STRING);
			if (index != -1) {
				content = content.substring(index + CHARSET_STRING.length())
						.trim();
				if (content.startsWith("=")) {
					content = content.substring(1).trim();
					index = content.indexOf(";");
					if (index != -1)
						content = content.substring(0, index);
					if (content.startsWith("\"") && content.endsWith("\"")
							&& (1 < content.length()))
						content = content.substring(1, content.length() - 1);
					if (content.startsWith("'") && content.endsWith("'")
							&& (1 < content.length()))
						content = content.substring(1, content.length() - 1);
					ret = findCharset(content, ret);
				}
			}
		}
		return ret;
	}

	public String findCharset(String name, String _default) {
		String ret;
		try {
			Class<java.nio.charset.Charset> cls;
			Method method;
			Object object;
			cls = java.nio.charset.Charset.class;
			method = cls.getMethod("forName", new Class[] { String.class });
			object = method.invoke(null, new Object[] { name });
			method = cls.getMethod("name", new Class[] {});
			object = method.invoke(object, new Object[] {});
			ret = (String) object;
		} catch (NoSuchMethodException nsme) {
			ret = name;
		} catch (IllegalAccessException ia) {
			ret = name;
		} catch (InvocationTargetException ita) {
			ret = _default;
			System.out
					.println("unable to determine cannonical charset name for "
							+ name + " - using " + _default);
		}
		if (ret.equalsIgnoreCase("gb2312"))
			ret = "GBK";
		return ret;
	}

 在这里我想特别说一下,最后一句:

if (ret.equalsIgnoreCase("gb2312"))
	ret = "GBK";

 如果你实现的是简繁转换功能的话,请加上这句,因为如果网站是gb2312编码,有些繁体字显示转换出来会是乱码 ‘?’.

 

 

 2:处理相对路径。
 相对路径,不管是图片,还是链接,或是JS,VBSCRIPT,CSS等,都会出现相对路径的写法,我现在将一些最常见的相对路径罗列一下。
 假如我们的目标网站是 http://tieba.baidu.com/aaa/bbb/ccc.htm
 假如其页面源代码中的有一幅图片是
 <image src=”pic.gif”>
 那么我们遍历到该image标签时,为了拼接HTML代码后图片可见,必须将代码改成
 <image src=” http://tieba.baidu.com/aaa/bbb/pic.gif”>,即补齐它的图片路径.
 同样
 <image src=”/pic.gif”>
 转换后<image src=”http://tieba.baidu.com/pic.gif”>
 <image src=”./pic.gif”>
 转换后<image src=”http://tieba.baidu.com/aaa/bbb/pic.gif”>
<image src=”../pic.gif”>
 转换后<image src=”http://tieba.baidu.com/aaa/pic.gif”>
<image src=”../../pic.gif”>
 转换后<image src=”http://tieba.baidu.com/pic.gif”>
 也就是说只需写一个简单的算法,根据网站URL,将相对路径转成绝对路径就可以了。

 

 

3:处理普通图片、背景图片、样式表图片
普通图片即是类似 <image src=”pic.gif”>的方式,这个遍历时判断结点是否是ImageTag就可以了。
背景图片就不好判断了,可能有背景图片的标签有很多,如BODY,TD,TR,TABLE,DIV等,这些标签,一种方法是:我们必须先判断它有没有 background属性,如果有,将其转为绝对路径。这种方法不能满足所有情况,因为背景图片可能不是以background属性写出来,如下的写法也可达到一样的效果。

<body style="background-image:pic.gif"> 

 

这样的写法,我们无法准确找到pic.gif字符串并转换它。
样式表图片跟这个差不多的问题,有些CSS文件里的图片也是按相对路径的写法来写,根本准确取得描述图片的字符串。
此时需要借助正则表达式来解决,专用来处理图片。

String REGEXP_HREF = "[\\w|\\/|\\.]+(http)?(s)?(\\:\\/\\/)?[\\:|\\.|\\/|\\w|\\d|_|\\'|+|-]+\\.(gif|jpg|png|bmp)

当读取可能出现背景图片的标签时,将此标签.toHtml()代码用正则表达式处理替换。这样所有的相对路径图片才能替换成绝对路径。

 

 

4:处理JS,VBS
处理JS可以说是很难的一部分,因为大多数情况下JS会有展现的任务,即是说页面上会有一些部分是引用的JS展现的。比如某些网站的头和尾。大多如下:

<script src="/top.js"></script>

此时我们先将相对路径转为绝对路径
假如目标网址是:http://tieba.baidu.com/aaa/bbb/ccc.htm,转换后的JS是这样

<script src="http://tieba.baidu.com/top.js"></script>

 但是,单纯将JS的路径补齐是没有用的,它并不像图片那样显示完以后就没事了,JS代码也会包含简体中文,包含相对路径,这也是需要转换成繁体中文和绝对路径的,所以这个链接我们还得在它前面加上一串,变成

<script src="http://www.mydomain.com/big5/http://tieba.baidu.com/top.js"></script>

还有问题,编码问题,script标签其实有个encoding属性,默认值是当前页面的编码,如果有值就必须用encoding里值的编码解析JS。
为避免混乱,可以为解析JS专门写一个解析的方法,并且这种方法跟我们按结点编历网页的方法不同,解析JS最好按一行行进行读取,因为JS毕竟不是完全意义上由结点构成的。
解析JS的方法我们也得配置一个URLrewrite,假如编码是GBK,上面的链接最终改成

<script src="http://www.mydomain.com/big5js/GBK_http://tieba.baidu.com/top.js"></script>

 由于是一行一行读取,无法精确取得链接地址,和图片地址
所以也需要用正则表达式来替换
其实有些用JS写出来的路径是无法用正则表达式匹配到的,这时之前做的URL转换会起作用了,它会自动将地址栏的路径换算成相对路径补齐显示。

 

 

5:处理XML
有些网站的列表页打开时会默认读XML文件展示,标题,链接等都是从XML文件读出来,XML文件中也会简体中文和链接,也必须进行转换。
同样的,还是编码问题,XML的编码好判断,但会有麻烦事,UTF-8问题,有些UTF-8的XML文件BOM问题,你会发现有的XML你始终不能正确读取。在网上有前人写了个UnicodeReader的类可以解决这个问题,大家可以去找找这个类

BufferedReader br = new BufferedReader(new UnicodeReader(newURL
					.openStream(), "UTF-8"));

解析完XML以后,不能像HTML或JS一样,推送到一个空白页,可以将它放在PrintWirte里,这样页面才能调取到。

 

 

6:处理链接
正常的链接一般是

<a href="/index.htm">link</a>

 用户在繁体页上点击这个链接后,出来的页面也应该是繁体,所以需要将链接也转换一下,最终的写法是:

<a href="http://www.mydomian.com/big/http://tieba.baidu.com/index.htm">link</a>

 其实这就已经完事了,我之所以单独把处理链接拿出来说,是因为有时写法并不是这样。
例如:

<a href="javascript:goUrl('/index.htm')">link</a>
<a href="#" onclick=”goUrl('/index.htm')">link</a>

 这个时候我们按正常的流程也没办法取得准确的链接字符串,有两个办法,一是将这个LinkTag.toHtml()用正则表达式替换一次,二是将可能出现URL的属性替换一次,第二种方法虽然显得麻烦,但碰到大多数正常的A标签效率会很快。
我还碰到过一个问题,页面上有锚点,A标签根本就没有href这个属性,所以处理A标签时最好先判断href属性是否存在,其实处理大多数结点时都应这样操作,因为HTML里未知的因素和写法太多太杂了。

 

 

7:处理文本
所谓简繁转换,就是要将文本中简体中文变成繁体中文,在这里文本大多数情况指的是TextNode,我们需要一个简体转繁体的方法,每当遍历到TextNode时将字符串送进去,取得返回值就可以了。现在网络上流行很多种简转繁的方法,大多是字对字转换,整词或语义转换一般都是商业级别的。
需要说明的事,一个页面上出现的中文字并不全是TextNode类型的,
比如按钮上的字(value属性),一些提示性的文字(alt属性)等,这些都是需要进行转换的,这些可以在遍历到那个结点时进行转换。

 

 

8:设置繁体网页的编码
如果你程序采用的是UTF-8,并且是用UTF-8推送到页面上的,你需要将繁体页面的编码写成UTF-8,即<meta content=”text/html;charset=UTF-8”>
我看到很多成功案例的繁体页面的编码写的是Big5,其实写Big5也可以,不过可能往后在表单的转码上会有一些问题。

 

 

9:处理表单
这应该是简繁转换最难的一部分,转成繁体后的网站表单理论上应该可以接收简体输入和繁体输入,但它接收值后的控制层是我们不能干涉的,并且一个网站的表单可能是从很多别的网站引过来,控制层的编码也可能不同。
处理这样的表单,我们需要做的事情是将用户的输入转换成该表单可转换的编码然后再发送过去。
我们必须将form的action地址转换掉,让用户直接提交到我们的action里
在处理用户请求之前,我们还得知道原先action的编码,
然后将接收到的参数值先进行繁转简,再用URLEncoder转换成相应编码的字符串

StringBuilder requestParameters = new StringBuilder();
		Enumeration parameterNames = request.getParameterNames();
		while (parameterNames.hasMoreElements()) {
			String name = (String) parameterNames.nextElement();
			//过滤掉重复的和不需要的参数
			if(name.equals("url")||name.equals("methodname"))
				continue;
			String value = request.getParameter(name);
			String[] values = null;
			if (value == null) {
				// 判断是否是数组类型参数
				values = request.getParameterValues(name);
			}
			if (value != null) {
				//System.out.println("name:"+name+"value:"+value);
				value = ChineseToBig5.convert_tosimple(value);
				requestParameters//
						.append("%26"+URLEncoder.encode(name, encoding))//
						.append("=")//
						.append(URLEncoder.encode(value, encoding))//
				;
			} else if (values != null) {
				for (String s : values) {
					s = ChineseToBig5.convert_tosimple(s);
					requestParameters//
							.append("%26"+URLEncoder.encode(name, encoding))//
							.append("=")//
							.append(URLEncoder.encode(s, encoding))//
					;
				}
			}
		}

接着将参数和参数值拼接好,发送到原form的action中.
如果我们不关心表单提交后的返回页面是简体或是繁体,可以采用如上步骤。
当处理类似站内搜索的表单时,用户希望搜索后的返回页面也是繁体的,那得将我们的逻辑再加强一下,将参数和参数值拼接好发送到原from的action时,把拼接好的带参数的URL看成一个普通的页面在最前面加上我们简转繁的方法就可以了。

 

 

10:设置过滤
设置过滤有两种情况
第一种是页面上会有一些特殊的地方是不需要进行URL转换的,比如网站上  “简体版”和“繁体版”两个链接,如<a href=”http://tieba.baidu.com”>简体版</a>,这个URL是不需要进行转换成 http://www.mydomain.com/big5/http://tieba.baidu.com 的,因为用户点击它会直接访问而不需要转换。这个需要修改一下页面代码,用一个自定义的标签包围,如:

<MyTag><a href="http://tieba.baidu.com">简体版</a></MyTag> 

Htmlparser有自己的增加和处理自定义标签的方法,逻辑可以这样,当程序遍历时,发现MyTag标签,里面的内容不转换。
另一种过滤是指网站级的过滤。
如,我们提供的简繁转换只想提供给http://www.baidu.com 及其所有二级域名
可以建一个配置文件
在里面设置值  urllist=http://*.baidu.com
然后在action中,每当接收到url值时,与http://*.baidu.com匹配一下,不能匹配则直接跳转,可以匹配则遍历转换。
这里的匹配我详细说明一下:
Action里接收的目标url不管是什么样的,都可以取得其根域名,这个算法在网上有很多,
例如 http://tieba.baidu.com/aaa/bbb/fadsfadsf.htm 的根域名是 http://tieba.baidu.com
取得根域名后,将它与我们在配置文件里设置的值http://*.baidu.com进行匹配,这个匹配是一个叫“两字符串求公共字串”的算法,我把它的使用方法改了一下。
原来的效果是
1 - http://tieba.baidu.com/
2 - http://mp3.baidu.com/abc.htm
1和2求公共子串后得出的字符串是 http://*.baidu.com/*
我在过滤时,将得到的域名和原先设置好的url值求公共子串,如果返回值仍等于url里设置的值,那么我认为这个网站可以被转换,否则就跳转。

 

 

11:提高效率
毫无疑问,每多一个判断会就会阻碍一些性能,特别是那些大型门户网站的首页。
其实我最终写出的程序在没有设置任何缓存的情况下速度也是很快的,由于我公司最近网速不稳定,所以测试可能不够准确,但无论怎样,肯定是得加上缓存的。
我现在采用的是squid,其实一个网站浏览繁体页面的用户数量是很少的,缓存时间稍微设置长一点也是可以接受的。

 

 

差不多就是这样了,以上内容是我一下午凭记忆打出来的,不知道有没描述清楚,可能有很多遗漏和没有提及的,大家将就着看,我在最开头的时候说这种即时的转换不能涵盖所有的情况,因为HTML的写法千奇百怪,有些JS的写法更是出奇的怪异,百分之百的功能还原和网页还原是不现实的,很多网页的简转繁都有这样那样的涵盖不了的问题,希望大家能提宝贵意见,可以尽量让算法更具兼容性。

分享到:
评论
2 楼 leadyu 2009-04-03  
shingo7 写道
没有人观注抓取网页这一块么。

繁简互转,几年前国内就有成熟产品了,从当初几万一套,卖到现在几百一套,LZ可以google一下。

繁简转换,最麻烦还是分析HTML,还有就是误字,繁体中有些字不是直接翻译简体就对了。
1 楼 shingo7 2009-04-02  
没有人观注抓取网页这一块么。

相关推荐

    asp简繁转换功能插件

    【ASP简繁转换功能插件】是一款针对网页内容...总的来说,ASP简繁转换功能插件是一个方便快捷的解决方案,它简化了网站的语言适配过程,使得开发者能够快速地为用户提供简繁体切换的选项,从而增强网站的国际化体验。

    汉子简繁转换器,支持汉子转换,小写英语转Pascal, 暂时不支持word和excel

    《汉字简繁转换器:轻松应对文字转换挑战》 在信息技术高速发展的今天,语言处理工具在日常...尽管目前存在一些限制,但随着技术的不断进步,我们可以期待更完善的转换解决方案出现,以满足日益多样化的语言处理需求。

    Java软件 简繁转换易jar程式嵌入工具

    Java软件简繁转换易jar程式嵌入工具是一款实用的文本处理工具,利用Java的跨平台特性和强大的文本处理库,实现了简体中文与繁体中文之间的高效转换,对于需要处理中文文本的开发者和用户来说,无疑是一个便捷的解决...

    维基百科中提取的简繁转换代码

    总之,维基百科的简繁转换代码提供了一种实用的解决方案,对于需要在简体和繁体中文之间切换的项目来说,是宝贵的资源。深入研究和理解这段代码,不仅可以提升对中文字符处理的理解,也有助于开发出更高效、更准确的...

    JS 简繁转换

    描述提到"支持多种浏览器(IE、火狐、谷歌、Safari等)",这意味着这个JS解决方案考虑到了跨浏览器的兼容性问题。在Web开发中,不同的浏览器可能对某些JS特性有着不同的支持程度,因此编写能够兼容多种浏览器的代码...

    PPT2003 简繁转换补丁

    为此,"PPT2003简繁转换补丁"应运而生,为用户提供了便捷的转换解决方案。 简繁转换补丁是专门针对PPT2003设计的一个插件,它能够帮助用户在创建或编辑PPT文件时实现文本的自动或手动简繁转换。这个补丁的出现,极...

    简繁转换工具(绿色安全免安装).rar

    本文将详细介绍一款名为“简繁转换工具”的绿色安全免安装软件,它以其小巧的体积和高效的功能,为用户提供了便捷的简繁转换解决方案。 简繁转换工具,正如其名,是一款专注于简体字与繁体字相互转换的应用程序。它...

    汉字转拼音与汉字简繁转换

    总之,汉字转拼音与汉字简繁转换是中文信息处理中的关键技术,微软提供的解决方案在易用性和效率上都有很高的评价。无论是为了学习、工作还是娱乐,这些工具都能极大地帮助我们跨越语言障碍,促进信息的自由流动。...

    VB6 简繁转换源代码(非内码转换)

    总结来说,这个VB6源代码提供了一种在简体系统下的简繁转换解决方案,但不适用于编码转换,并且可能依赖于简体中文环境。在使用和修改这段代码时,需要注意它的工作环境和限制,以及正确处理字符编码问题,尤其是当...

    批量简繁转换

    为了应对这种需求,批量简繁转换工具应运而生,它旨在为用户在处理大量文本数据时提供方便、高效的解决方案。 批量简繁转换工具的主要特点在于其强大的转换引擎,这种引擎通常基于深度学习和自然语言处理技术。依靠...

    简繁转换王(626kb)

    《简繁转换王》以其626kb的精巧体积,为用户提供了一个高效的解决方案。其使用方法极为简便,用户仅需将需要转换的文本复制到软件中,随后点击转换按钮,即可得到所需的简体或者繁体文本。它既可以处理简体转繁体,...

    html js简繁转换

    本项目“html js简繁转换”旨在实现一种基于JavaScript的解决方案,允许用户在HTML页面上轻松地切换简体与繁体文字,无需刷新页面。这个功能对于具有多语言环境的网站尤其有用,它提升了用户体验,使用户可以按照...

    wp-chinese-switcher:文派简繁切换器(WP Chinese Switcher),完全基于您WordPress网站服务器端的中文繁简转换解决方案

    WP中文切换台文派简繁切换器(WP Chinese Switcher),完全基于您WordPress网站服务器端的中文繁简转换解决方案。此项目分叉于原WP中文转换中文简繁转换器免费插件,感谢原作者Ono Oogami提供了此工具。由于原插件...

    简繁转换通简繁转换通简繁转换通简繁转换通

    在使用《简繁转换通》之前,用户应该仔细阅读这份文件,了解如何安装、启动和使用软件,以及可能遇到的问题和解决方案。这对于初次接触此类工具的用户来说,是非常重要的参考资料。 简繁转换通的使用流程一般包括...

    简繁互换

    综上所述,这个压缩包提供了一个简繁中文转换的解决方案,包含源码和必要的资源文件。开发者可以通过研究这些源码和配置文件,理解并定制简繁转换的逻辑,或者直接在自己的项目中集成这个组件。在实际应用中,这种...

    简繁转换器 For PJBlog2.rar

    标题中的“简繁转换器 For PJBlog2.rar”表明这是一个专为PJBlog2博客系统设计的插件,用于实现简体中文与繁体中文之间的转换功能。PJBlog2是一款流行的开源博客程序,允许用户轻松创建和管理个人或团体博客。这个...

    twTrCn-简繁转化php插件

    2. `transfer.class.php`:这是核心类文件,包含了实现简繁转换的具体逻辑。在这个文件中,很可能定义了一个名为`Transfer`的类,类中会有如`convert`这样的方法,用于执行实际的字符转换工作。类内部可能使用了数组...

    Java简体繁体转换.rar

    本资源"Java简体繁体转换.rar"提供了一个基于Java的解决方案,用于进行简体中文到繁体中文的转换,以及繁体到简体的转换。以下是关于这个主题的详细知识: 1. **Java语言基础**:Java是一种广泛使用的面向对象的...

    中文简繁通(GBK简体、GBK繁体、BIG5转换)

    转换工具“中文简繁通”提供了一个方便的解决方案,使得用户可以在不同编码格式之间进行转换,这对于跨地区、跨平台的信息交流非常有帮助。例如,如果你在一个使用GBK编码的系统中接收到一份BIG5编码的文档,这款...

    Chinese Language简繁汉字内码转换组件

    无论是服务器端的数据处理,还是客户端的显示需求,这个组件都能提供稳定且高效的解决方案。通过深入理解并合理利用这个组件,开发者可以更好地应对多语言环境的挑战,提升软件或网站的用户体验。

Global site tag (gtag.js) - Google Analytics