“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
从本主题开始《云客Drupal8源码分析》系列将连续发布和前端js相关的内容,如果您对JavaScript还不熟悉或者需要来一次系统性的整理回顾,在此云客为您准备了以下资料:
首发于爱码文档汇(nowicode.com),您也可以到云客的博客阅读,该篇资料全文6万字,A4页面45页,简明系统的介绍了js相关知识,由于从Drupal8.4开始核心为每一个js文件加入了对应的ES6版本,因此该资料也全面补充介绍了ES6,该资料对比了php和js语言,是针对php开发者而写的,在继续阅读本系列前强烈推荐您阅读。
前端js使用翻译:
本系列已经讲述了php程序和twig模板中的翻译如何处理,请见本系列的以下主题:
《国际化Internationalization:核心翻译系统》
《twig服务》
在前端的js程序中可以使用以下方法进行翻译:
Drupal.t(str, args, options):
返回单数翻译,参数解释如下:
str:为要翻译的字符串
args:为可选的翻译参数,一个对象,属性名的第一个字符有特殊含义,如果是“@”那么意指属性值是一个原始文本,里面不会存在标签,将先用Drupal.checkPlain(str);处理后再替换;如果是“!”那么代表属性值是安全的,将直接替换;如果既不是“@”也不是“!”,那么将被Drupal.theme('placeholder', args[key])处理后再替换
options:为选项参数,其context属性可以指定翻译上下文
Drupal.formatPlural(count, singular, plural, args, options):
复数翻译,参数含义如下:
count:代表传递的数量
singular:代表单数时的字符串
plural:代表复数时的字符串
args:代表需要替换的额外占位符,和单数翻译中的含义一样,内部属性名不用使用“@count”,该属性名被保留来存放参数count的值
options:代表选项,比如指定上下文,该方法内部使用单数翻译方法
示例如下:
Drupal.t('Enabled');
Drupal.formatPlural(5, '1 new comment', '@count new comments')
以上两个翻译方法定义在核心库“core/drupal”中,因此需要用到翻译的js在库声明中应声明对其的依赖,这样才能使js在其后执行;该库在文档加载阶段被立即执行,因此如果你的js是在异步阶段执行的,也可以不用声明依赖
翻译原理:
翻译数据来自哪里呢?你可能会想是通过ajax获取的,但实际上不是,页面加载了一个翻译数据文件,默认类似如下:
<script src="/sites/default/files/languages/zh-hans_G0IT0RIYYhJK8iKLwxkqQH25OXr1R25YPE5Q4aeqUkY.js?prmq4x"></script>
该js文件在核心库“core/drupal”之前加载,建立了一个纯数据全局对象:window.drupalTranslations,称为翻译数据对象,包含了系统中已加载过的全部js程序中会用到的所有当前语言的翻译数据,而不仅仅是本次请求加载的js文件,没有被翻译的源字符串不被包含(见下),翻译数据对象格式如下:
strings.上下文.源字符串=翻译字符串
pluralFormula={ 1: 0,default: 1}
没有上下文的翻译源字符串的上下文值为空字符串“""”,属性pluralFormula用于指示当前语言支持多少种复数形式,用单复数分隔符将翻译后字符串分隔后,用该变量指示对应复数字符串在数组中的位置,大多数语言为{ 1: 0,default: 1},意为单数时采用第一个数组元素,其他形式的复数(多于一个时)一律采用默认值,也就是第二个元素,程序内部使用,来源于“locale.plural.formula”服务
翻译数据文件:
翻译数据文件是一个纯粹用于保存数据的js文件,那么她来自哪里呢?这要从locale模块的translations库说起,见:
core/modules/locale/ locale.libraries.yml
该库仅声明了一个js文件:locale.translation.js
实际上该文件并不存在,仅作为一个占位符而已,她将在以下修改钩子中替换成真实的js翻译数据文件:
function locale_js_alter(&$javascript, AttachedAssetsInterface $assets)
产生并返回翻译数据文件涉及多个函数,介绍如下:
function locale_js_alter(&$javascript, AttachedAssetsInterface $assets)
用真实的翻译数据文件替换“locale/translations”库中定义的占位符,并保证在core/misc/drupal.js之前被加载,如果不存在翻译数据文件,那么不加载(删除占位符)
function locale_js_translate(array $files = [])
返回翻译数据文件的绝对路径(流包装器方式“public://”),如果不存在则返回null,参数$files为本次请求中加载的所有本地js文件(不包括翻译数据占位符文件),是一个索引数组,键值为js文件的绝对路径。该方法将查看是否有新增的js文件,如果有则解析提取里面的翻译源字符串和上下文到数据库翻译相关表中,以供管理员翻译,并重建翻译数据js文件,每个js文件只会被解析一次,理解该方法需要注意以下三个数据的含义:
翻译数据文件默认目录($dir = 'public://' . \Drupal::config('locale.settings')->get('javascript.directory'); ):
翻译数据文件默认储存目录为“public://languages”,也就是:/sites/default/files/languages
系统中已解析的全部js文件(\Drupal::state()->get('system.javascript_parsed') ):
是一个索引数组,保存已被解析过的js文件的绝对路径,另外可包含刷新旗标:如果其中存在“refresh:语言id”这样的键名(值为“waiting”),说明该语言对应的翻译数据文件需要刷新重建
翻译数据文件哈希(\Drupal::state()->get('locale.translation.javascript') )
保存各语言的翻译数据文件哈希值,是一个数组,键名为语言id,键值为对应的翻译数据文件的哈希值,来自文件内容的哈希运算,被用于翻译数据文件的文件名:“默认目录/语言id_哈希值.js”如:
/sites/default/files/languages/zh-hans_G0IT0RIYYhJK8iKLwxkqQH25OXr1R25YPE5Q4aeqUkY.js
function _locale_parse_js_file($filepath)
参数为本地js文件的全路径(以core开始),读取并用正则表达式解析这个js文件,将翻译方法Drupal.t和Drupal.formatPlural的参数提取出来,也就是将翻译源字符串和翻译上下文提取出来,然后保存到数据库的翻译数据表中,这样管理员就可以在后台为其添加翻译了,由于是正则表达式解析,在书写js时有限制,见文末补充说明。
function _locale_invalidate_js($langcode = NULL)
在已解析js文件数组中设立某语言的刷新旗标,被设置了刷新旗标的语言,其翻译数据文件将被重建,旗标是在已解析js文件数组中添加键名“refresh:$langcode”,键值为“waiting”,重建后该旗标会被删除。如果没有传递语言参数,将为系统中所有语言设置刷新旗标,如果英语被设置为不可翻译的,在参数为空时不会为英语设置旗标
function _locale_rebuild_js($langcode = NULL)
重建某个语言的翻译数据文件,参数为语言代码,如果没有被传递将为当前语言重建,返回布尔值,表明翻译数据文件重建后是否已经存在可用了(如果不需要翻译数据文件时会返回true)
翻译数据文件中的翻译数据,仅包含本语言的有翻译的数据,且翻译源字符串来源是javascript,也就是从js文件中提取的翻译源字符串,未被加载过的js文件中的翻译不存在;文件名是“语言id_哈希值”,其中哈希值是对文件内容执行以下操作的结果:
\Drupal\Component\Utility\Crypt::hashBase64($data);
哈希值会被保存到状态信息中,见上文。
缓存问题:
翻译数据js文件重建后形成新文件,文件名将改变,原文件会被删除,没有失效任何缓存标签,这对页面缓存有什么影响呢?在drupal内部针对响应对象(整页面,而非局部渲染数组)的缓存有两级:
动态页面缓存(服务id:dynamic_page_cache_subscriber)
这是针对任意用户的动态缓存,在派发请求和响应事件时执行,由于动态页面缓存中被缓存的响应尚未将js占位符替换成js文件,因此翻译数据js文件不论是否重建,均不影响该缓存
匿名页面缓存(服务id:http_middleware.page_cache)
这是针对匿名用户的缓存,在HTTP核心堆栈中执行,翻译数据js文件重建后文件名发生改变,这将导致被缓存的页面无法加载到,因此需要手动失效该缓存
比较大的项目在系统外部经常也会设置缓存,如多级缓存服务器,在这些外部缓存中,如果页面和js文件不是一起缓存的,将会受到重建影响
翻译数据js文件重建操作没有考虑到任何缓存问题,没有失效任何缓存标签,这是一个需要改进的地方,云客已向官方建议,解决办法有两个:
1、在响应事件订阅器“finish_response_subscriber”中为每个可缓存响应设置了'http_response'缓存标签,当发生重建时可失效该标签以使页面正确加载
2、翻译数据js文件采用固定文件名,不使用哈希,这是首选方法,不影响性能
补充:
1、在drupal的前端,如果是引入外部js,那么无法为其提供翻译功能,这并不是说外部无法使用翻译方法,而是外部js使用drupal的翻译函数也可能无法翻译,因为外部js没有经过翻译源字符串数据的提取,所以可能不存在翻译数据,如果系统中已经存在翻译数据了,那么是可以正常使用的。
2、系统在提取js文件的翻译源数据时,采用正则解析方式,因此不能深入理解js的逻辑,在使用翻译方法Drupal.t和Drupal.formatPlural时,参数不能使用变量名,需要采用字面量,否则无法提取源字符串供后端管理员翻译,这仅是无法提取而已,并不妨碍js执行,如果系统已经有翻译数据,那么即便采用变量做参数也是可以执行翻译的;此外这种提取方式即便翻译方法位于注释块中也会被提取,由于这些不完美,云客建议drupal改进提取方法,比如像插件释文一样,在页头添加翻译注释元数据,但这会给js开发者带来额外工作,是否值得有待商榷。
3、翻译数据js文件不可被随意删除,其仅在遇到没有被解析过的js文件时才自动重建,如果不小心删除了,可调用一次失效方法_locale_invalidate_js,或直接调用_locale_rebuild_js($langcode = NULL)方法重建。每个js文件仅会进行一次翻译源字符串的解析提取,如果有变动,比如新加了翻译,那么需要在状态系统中从已解析文件列表中删除,这样就能再次解析提取了
4、在后端的翻译函数中能通过选项参数(键名langcode)指定要翻译的目标语言,前端无此功能,仅能被翻译为当前页面语言,前端的选项参数中仅上下文context参数可用。
反馈互动