150. 接口翻译导入导出与删除

本篇讲述接口(界面)翻译(Interface Translation)数据的相关处理,也就是通常所讲的UI文本翻译,不包含内容、配置翻译等,关于系统翻译原理请见本系列翻译系统篇。

PO文件:

即“.po”扩展名的文件,是可移植对象“Portable Object”的缩写,用于解决文本国际化(翻译)问题,换句话说,即用来储存翻译数据,在很多国际化软件中都能见其身影,源于GNU计划的gettext项目,见:

http://www.gnu.org/software/gettext/

使用文档页:

http://www.gnu.org/software/gettext/manual/html_node/index.html

PO文件”的内容包含了翻译源字符串、目标字符串、上下文、单复数等信息,格式见以下文档:

  http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-F…

Drupal中即用po文件来导入、导出翻译,一个po文件储存一种语言翻译

 

翻译数据的获取和建立:

获取翻译:

Drupal用户通常从官方翻译服务器下载po文件,地址为:

https://localize.drupal.org/

在该服务器可以得到drupal安装版(包含了所有核心模块)的翻译:

点击“Downloads”后找到所需语言,以及系统版本,点击即可下载

还可以得到社区贡献模块的翻译:

点击“Downloads”后在顶部的“Pick a project”处输入模块名(或输入一部分再根据系统提示进行选择),然后点击“Show downloads”,即会显示相关语言和版本供用户点击下载

也可在首页右上角的“And/or pick a project 处输入模块名进入翻译页面,然后点击“Export”进行更详细的下载控制

得到的文件名格式通常如下:

{project}-{version}.{langcode}.po

由项目名、版本号、语言代码组成,比如:drupal-9.0.0.zh-hans.po,下载po文件后导入站点即可(见下文)。

这里需要解释一下项目名project的含义:

项目名指示模块或主题所属的项目,在其info文件的“project”根键中指定,多个模块可以同属于一个项目,只需要她们info文件的project项有相同值即可,典型的有“examples”模块、“commerce”模块,这两模块有许多子模块,这些子模块和主模块同属于一个项目;核心所有模块同属于一个项目,项目名为“drupal”;

info文件中“project”根键是可选的,默认为空字符串,如果是放置在官网的贡献模块,在没有设置该项的情况下,会被打包器自动设置为模块机器名,翻译和系统更新均是以项目为单位进行处理的

建立翻译:

一般建立翻译的人员有用户自己、翻译贡献者、模块作者;用户自己可以在后台以下地址自行翻译:

/admin/config/regional/translate

这在校正翻译和补充翻译时很有用

翻译贡献者和模块作者可以到官方翻译服务器针对安装版或具体模块贡献翻译,模块作者也可以随模块一起提供翻译,详见下文。

翻译源字符提取:

以上翻译服务器中的翻译源字符串是从程序代码中直接提取的,上传到官网的贡献模块均会被自动提取,开发者需要注意:提取是通过正则表达式对程序文件进行静态特征分析进行的,即查找t()format_plural()函数并进行正则参数解析,系统并没有更智能更复杂的提取处理,如果开发者在这些函数的源字符串参数中传递变量而不是字面量,那么将导致无法提取,因此建议开发者尽量避免这样做,无法提取将导致无法在翻译服务器上翻译,但在系统后台依然可以进行翻译。

 

管理界面导入、导出:

导入地址:/admin/config/regional/translate/import

导出地址:/admin/config/regional/translate/export

用户自己在系统后台进行的翻译往往不想被下载的翻译覆写,因此需要更高的优先级,所以系统可将用户自己提供的翻译数据(文件),标记为“自定义”翻译,对应的,如果翻译数据(文件)来自官方社区(翻译服务器localize.drupal.org)或模块自带,那么这种翻译称为“非自定义”翻译,翻译数据的这种属性被系统保存在数据库表“locales_target”的“customized”中,这样就可以在导入翻译时,给用户一个是否覆写自己提供的翻译的选择。

在导入翻译时:

翻译文件本身并不表明自己是否为自定义,用户可以指定以下选项:

是否将导入的翻译当做自定义翻译

是否覆写非自定义翻译

是否覆写存在的自定义翻译

在导入时,如果指定的语言和po文件中真实的翻译语言不一致时,将以指定的语言为准

在导出翻译时:

导出的翻译也并不包含是否为自定义,可以指定以下选项:

是否导出非自定义翻译

是否导出自定义翻译

是否包含无翻译的源文本

 

新装模块后的翻译处理:

在模块被安装后系统会自动为其导入翻译吗?在同时满足以下4个条件时将会导入:

1、以下配置项被打开(默认是打开的,模块可修改),即值为true

\Drupal::config('locale.settings')->get('translation.import_enabled')

2、系统中有可翻译的语言,即是多语言网站或英语被设置为可翻译的

3、系统不处于初始安装阶段,即实例化站点阶段(安装drupal阶段)

4、被安装模块建立了新的项目时,具体讲即被安装模块申明了项目且项目名和模块机器名相同,这样的模块通常是项目的主模块,反过来讲一个项目仅在主模块被安装时才执行导入,子模块不会执行,翻译是以项目为单位进行导入和更新的,并不以模块为单位,翻译文件应包含项目中所有模块的翻译数据。

 

前文已讲到声明项目是通过info文件的“project”根键指定,位于官网的贡献模块在没有指定该项时会自动添加,值指定为模块机器名,项目名也用于系统更新,接口翻译模块允许我们不通过project声明项目,而通过以下键为翻译功能专门指定项目名:

interface translation project

该项的值必须和模块机器名相同才会执行导入,一旦设置,将覆写“project”项申明的项目名,而不论“project”是否被设置或值如何,该项的意义在于为翻译导入功能模拟项目名,让我们在不真正申明项目名的情况下,模块安装后依然可以导入翻译。

那么翻译数据从哪里来呢?可以远程服务器或本地文件,这在info文件中设置以下键指定:

interface translation server pattern

该项的值是获取翻译po文件的地址,可以是一个网址、本地的相对或绝对地址、或者流包装器地址,为了更灵活,地址中可以使用以下占位符:

"%core":表示核心版本,如“8.x”
"%project":项目名
"%version":当前项目版本号,如: "8.1", "8.x-1.0",即模块声明的版本号
"%language":语言代码,如“zh-hans”

该项是可选的,默认值储存在以下配置中:

\Drupal::config('locale.settings')->get('translation.default_server_pattern');

模块可以修改该配置,无值时将采用以下值:

'https://ftp.drupal.org/files/translations/%core/%project/%project-%vers…';

当地址是远程地址时,系统会自动进行下载,并保存到流包装器“translations://”中,该流包装器的路径可以在以下管理后台配置

/admin/config/media/file-system

默认为:sites/default/files/translations

如果是一个本地地址,通常会采用流包装器“translations://”指定,当然也可以采用绝对或相对地址,或其他流包装器

如果是私用的自定义模块,因为我们知道PO文件的精确位置,因此可以采用任意地址,但如果是贡献模块,且想在模块文件夹中一起提供po文件,此时本地地址如何指定呢?由于模块有多个可能的安装位置,我们并不知道会被放置到何处,系统目前没有提供指示模块根目录的占位符,因此无法指定一个准确的本地地址,此时可以通过“hook_install()”钩子将模块目录中的po文件复制到“translations://”中,然后指定本地文件位置为翻译流包装器地址即可,如“translations://%project-%version.%language.po”,为了清晰的说明这个过程,这里提供一个示例:

模块安装后自动导入附带翻译示例:

假设模块名为“yunke”,在模块的“translations”根目录中提供模块各语言的翻译po文件,文件名格式如下(各部分以点号做连接):

%project.%language.po

模块info文件内容如下:

name: yunke
type: module
description: '模块被安装后自动导入翻译示例'
version: '1.0.0'
package: yunke
'interface translation project': yunke
'interface translation server pattern': 'translations://%project.%language.po'
core_version_requirement: '~8.8 || ^9'
datestamp: 1591272046

 

然后在根目录中建立“yunke.install”文件,里面放入以下两个文件:


/**
 * 将模块目录下的翻译文件复制到翻译流包装器目录中
 *
 * @param $moduleName
 * 模块名
 */
function _copy_translations($moduleName)
{
  $directory = '/translations/';
  $filename_mask = '/' . preg_quote('.po') . '$/';
  $moduleHandler = \Drupal::moduleHandler();
  if (!$moduleHandler->moduleExists('locale') || !$moduleHandler->moduleExists($moduleName)) {
    return;
  }
  $langcodes = array_keys(locale_translatable_language_list());
  if (empty($langcodes)) {
    return;
  }
  $translationsDirectory = $moduleHandler->getModule($moduleName)->getPath() . $directory;
  if (!is_dir($translationsDirectory)) {
    return;
  }
  $files = \Drupal::service('file_system')->scanDirectory($translationsDirectory, $filename_mask, ['key' => 'uri', 'recurse' => FALSE]);
  if (empty($files)) {
    return;
  }
  $translations = [];
  foreach ($files as $uri => $file) {
    $name = explode('.', $file->name);
    $langcode = end($name);
    if (in_array($langcode, $langcodes)) {
      $translations[$file->filename] = $uri;
    }
  }
  foreach ($translations as $filename => $uri) {
    \Drupal::service('file_system')->copy($uri, 'translations://' . $filename, \Drupal\Core\File\FileSystemInterface::EXISTS_REPLACE);
  }
}

/**
 * Implements hook_install().
 */
function yunke_install()
{
  $moduleName = substr(__FUNCTION__, 0, -8);
  _copy_translations($moduleName);
  return;
}

说明:

以上示例中,info文件内容向系统说明了需要导入翻译,并指定了翻译文件的位置和文件名格式,在安装钩子中实时从模块目录将翻译文件复制到info文件指定的位置,这样模块安装后就能自动导入模块提供的翻译文件了(具体在随后的locale_modules_installed($modules)钩子中进行导入),当模块卸载时,也会自动删除翻译流包装器中的po文件

被导入的翻译被视为非自定义的,是否会覆写存在的自定义或非自定义翻译呢?这取决于以下配置对象:

\Drupal::config('locale.settings')->get('translation.overwrite_not_customized');
\Drupal::config('locale.settings')->get('translation.overwrite_customized');

以上是系统提供的自动导入方法,通常也是我们最优先使用的方法,但在有些情况下我们需要以程序方式自行导入翻译,这怎么处理呢?(见下文)。

 

程序方式导入翻译:

前文讲述了系统提供的在模块安装后自动导入翻译的功能,该功能仅用于模块安装后的自动导入,这里我们采用自行实现,该实现可以用在任意地方,示例同样以模块安装后自动导入翻译为目标。

翻译的导入需要产生很多数据库连接,过程可能很漫长,为了避免php超时,一般会采用批处理方式进行,因此我们以批处理方式实现(如果需要在一个请求中完成可以将批处理修改为非渐进式即可)。

程序方式导入翻译示例:

为了模块安装后自动导入翻译,我们同样需要实现以下钩子:

  hook_install()

该钩子放置在模块根目录的“module.install”文件中,其被调用时,模块已经完全安装完成,同样假设被安装的模块机器名是“yunke”,翻译文件放置在模块根目录的“translations”文件夹中,文件名格式为“$langcode.po”,如“zh-hans.po”,此时钩子函数如下:
 


/**
 * Implements hook_install().
 */
function yunke_install()
{
  $filename = '/' . preg_quote('.po') . '$/'; //搜寻翻译文件的正则表达式 翻译文件为“$langcode.po”
  $directory = '/translations/'; //翻译文件所在的模块根目录
  $module = substr(__FUNCTION__, 0, -8); //要导入翻译的模块

  $moduleHandler = \Drupal::moduleHandler();
  if (!$moduleHandler->moduleExists('locale')) {
    return;
  }
  $langcodes = array_keys(locale_translatable_language_list());
  if (empty($langcodes)) {
    return;
  }
  $translationsDirectory = $moduleHandler->getModule($module)->getPath() . $directory;
  if (!is_dir($translationsDirectory)) {
    return;
  }
  $files = \Drupal::service('file_system')->scanDirectory($translationsDirectory, $filename, ['key' => 'name', 'recurse' => FALSE]);
  if (empty($files)) {
    return;
  }

  $translations = array_intersect($langcodes, array_keys($files)); //找出将导入的翻译文件
  $options = [
    'langcode'          => NULL,
    'overwrite_options' => [
      'not_customized' => true,
      'customized'     => true,
    ],
    'customized'        => LOCALE_NOT_CUSTOMIZED,
    'finish_feedback'   => false,
    'use_remote'        => false,
  ];
  $moduleHandler->loadInclude('locale', 'bulk.inc');
  $operations = [];
  foreach ($translations as $langcode) {
    $options['langcode'] = $langcode;
    $file = $files[$langcode];
    $file->project = 'drupal';
    $file->version = \Drupal::VERSION;
    $file->langcode = $langcode;
    $operations[] = ['locale_translate_batch_import', [$file, $options]];
  }
  $operations[] = ['locale_translate_batch_import_save', []];
  $operations[] = ['locale_translate_batch_refresh', []];
  $batch = [
    'operations'       => $operations,
    'title'            => t('Importing interface translations'),
    'progress_message' => '',
    'error_message'    => t('Error importing interface translations'),
    'file'             => $moduleHandler->getModule('locale')->getPath() . '/locale.bulk.inc',
  ];
  if ($options['finish_feedback']) {
    $batch['finished'] = 'locale_translate_batch_finished';
  }
  batch_set($batch);

  unset($options['langcode']);
  if ($batch = locale_config_batch_update_components($options, $translations)) {
    batch_set($batch);
  }

}

说明:

以上实现在模块安装后,会判断系统中存在哪些可翻译的语言,如果没有可翻译语言则什么也不做,如果有就会扫描翻译目录中是否提供了对应语言(以文件名为依据),当有提供时就会进行导入。

 

制作PO文件:

为了避免模块提供的po文件被直接下载,通常需要放置一个拒绝客户端访问的“.htaccess”文件在翻译目录中,你可以从配置同步目录中复制。

po文件需要头部,内容也有格式要求,为了便于读者快速制作,这里也提供一个内容范例:

# Chinese, Simplified translation
#
msgid ""
msgstr ""
"Language-Team: Chinese, Simplified\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"

msgid "yunke is from sichuan"
msgstr "云客来自四川"

# 示范变量
msgid "%name: may not be longer than @max characters."
msgstr "%name:不能长于@max个字符。"

# 示范双引号转义
msgid "\"On\" label"
msgstr "“开”标签"

# 示范复数形式
msgid "1 minute"
msgid_plural "@count minutes"
msgstr[0] "1 分钟"
msgstr[1] "@count 分钟"

# 示范上下文
msgctxt "Long month name"
msgid "May"
msgstr "五月"

# 更多格式请参考http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files
# 或者使用专用的Gettext编辑器

 

翻译数据清理:

系统在储存翻译数据时,并不记录源字符串来自哪个模块,如果多个模块提供了相同的源字符串,那么只会被记录一条,换句话说翻译是被所有模块共享,因为这个原因,模块在卸载时不能去清理自己提供的翻译,如果强行清理那么可能导致共享这些翻译的模块丢失翻译数据;同样的原因,系统也没有别的办法去清理翻译数据,因此系统没有提供翻译删除功能。

这就导致了一个问题:可能再也不会用到的翻译将一直驻留在系统中,时间流逝,不断有模块安装卸载,翻译数据会越来越多,垃圾数据也越来越多,因此强烈建议读者不要到生产站点去试安装模块,尤其是质量、来路存疑的模块。

如果真有一天无用翻译增长到无法忍受了怎么办?可以克隆站点,在克隆系统上删除全部翻译相关数据库表,再重新翻译全站后导出翻译文件,然后删除生产站点上全部翻译相关数据库表,最后导入之前导出的翻译文件。

思考:为什么不记录提供翻译源字符串的是哪个模块呢?因为翻译数据量很大,很常用,这样做将极大浪费性能

 

项目的翻译工作流设计:

在非英语国家,往往开发人员并不是最好的翻译人员,由于英语水平有限,通常需要借助翻译人员的帮助,这就导致了一个问题,在做开发时怎么进行翻译工作呢?这里以中文来做探讨,这不会失去一般性,假设开发者的英文书写水平很差,又难以提高英语水平:

如果每写一个T函数都去询问一下英语怎么表达,几乎是不可能做到的,即便是好几个文件写完再去问也很麻烦,读者可能会想到开发元语言可以不是英语(确实如此),是不是可以直接在T函数中用自己的语言呢?这样再提供英文的po文件就好了,这看起来曙光一现,但很快就会发现中文没有单复数概念,很容易用错翻译函数,如果还能勉强克服的话,当遇到有上下文信息的翻译时就可能会彻底失望了,比如中文的“可以”意思很清晰,没有歧义,但英文的“may”却需要传递上下文标识;开发元语言虽然可以不是英文,但翻译函数却是为英文设计的,看来在翻译函数中直接用中文行不通,还会为将来国际化协作埋下隐患。

推荐以下工作流程:

在用到翻译函数的地方留下todo记号,比如“//@todo 翻译:注释”,可暂时在函数中使用中文,等到项目最后阶段会同翻译人员一起处理这些todo标记,在IDE工具(比如phpstorm)的帮助下很容易找到它们,协同翻译时一并制作中文的po文件。

读者朋友们:除了让英语蹩脚的开发者提高英文水平外,您有更好的办法帮助他们吗?如何改进这个工作流程呢?

 

补充:

1、翻译相关官网文档地址:

https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Language…

https://api.drupal.org/api/drupal/core%21modules%21locale%21locale.api…

2、翻译导入后,系统默认会执行和翻译相关的更新,如刷新js翻译数据、配置覆写等

3、模块安装后系统会自动基于项目去更新或导入翻译,因为locale模块实现了hook_modules_installed(),在模块卸载时会检查是否需要删除po文件等,因为locale模块实现了hook_module_preuninstall()):

 

 

 

本书共158小节。


目前全部收费内容共295.00元。购买全部

评论 (0)