Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,13 @@
cache_strings: true
translate_english: false
javascript:
directory: languages
translation:
use_source: remote_and_local
default_filename: '%project-%version.%language.po'
default_server_pattern: 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po'
overwrite_customized: false
overwrite_not_customized: true
update_interval_days: 0
path: ''
import_enabled: true

View file

@ -0,0 +1,77 @@
id: locale
module: locale
label: 'Translation'
langcode: en
routes:
- route_name: locale.translate_page
tips:
locale-overview:
id: locale-overview
plugin: text
label: 'User interface translation'
body: 'This page allows you to translate the user interface or modify existing translations. If you have installed your site initially in English, you must first add another language on the <a href="[site:url]/admin/config/regional/language">Languages page</a>, in order to use this page.'
weight: 1
locale-language:
id: locale-language
plugin: text
label: 'Translation language'
body: 'Choose the language you want to translate.'
weight: 2
attributes:
data-id: edit-langcode
locale-search:
id: locale-search
plugin: text
label: 'Search'
body: 'Enter the specific word or sentence you want to translate, you can also write just a part of a word.'
weight: 3
attributes:
data-id: edit-string
locale-filter:
id: locale-filter
plugin: text
label: 'Filter the search'
body: 'You can search for untranslated strings if you want to translate something that isn''t translated yet. If you want to modify an existing translation, you might want to search only for translated strings.'
weight: 4
attributes:
data-id: edit-translation
locale-submit:
id: locale-submit
plugin: text
label: 'Apply your search criteria'
body: 'To apply your search criteria, click on the <em>Filter</em> button.'
weight: 5
attributes:
data-id: edit-submit
locale-translate:
id: locale-translate
plugin: text
label: 'Translate'
body: 'You can write your own translation in the text fields of the right column. Try to figure out in which context the text will be used in order to translate it in the appropriate way.'
weight: 6
attributes:
data-class: js-form-type-textarea
locale-validate:
id: locale-validate
plugin: text
label: 'Validate the translation'
body: 'When you have finished your translations, click on the <em>Save translations</em> button. You must save your translations, each time before changing the page or making a new search.'
weight: 7
attributes:
data-id: edit-submit--2
locale-continue:
id: locale-continue
plugin: text
label: 'Continuing on'
body: 'The translations you have made here will be used on your site''s user interface. If you want to use them on another site or modify them on an external translation editor, you can <a href="[site:url]/admin/config/regional/translate/export">export them</a> to a .po file and <a href="[site:url]/admin/config/regional/translate/import">import them</a> later.'
weight: 8
dependencies:
module:
- locale

View file

@ -0,0 +1,47 @@
# Schema for the configuration files of the Locale module.
locale.settings:
type: config_object
label: 'Translate interface settings'
mapping:
cache_strings:
type: boolean
label: 'Cache strings'
translate_english:
type: boolean
label: 'Enable English translation'
javascript:
type: mapping
label: 'JavaScript settings'
mapping:
directory:
type: string
label: 'Translation directory'
translation:
type: mapping
label: 'Translation settings'
mapping:
use_source:
type: string
label: 'Translation source'
default_filename:
type: string
label: 'Default translation filename pattern'
default_server_pattern:
type: string
label: 'Default translation server pattern'
overwrite_customized:
type: boolean
label: 'Overwrite customized translations'
overwrite_not_customized:
type: boolean
label: 'Overwrite non customized translations'
update_interval_days:
type: integer
label: 'Check for updates'
path:
type: string
label: 'Interface translations directory'
import_enabled:
type: boolean
label: 'Import enabled'

View file

@ -0,0 +1,133 @@
.locale-translate-filter-form .details-wrapper {
overflow: hidden;
}
.locale-translate-filter-form .form-item-langcode,
.locale-translate-filter-form .form-item-translation,
.locale-translate-filter-form .form-item-customized {
float: left; /* LTR */
margin-right: 1em; /* LTR */
margin-bottom: 0;
/**
* In Opera 9, DOM elements with the property of "overflow: auto"
* will partially hide its contents with unnecessary scrollbars when
* its immediate child is floated without an explicit width set.
*/
width: 15em;
}
[dir="rtl"] .locale-translate-filter-form .form-item-langcode,
[dir="rtl"] .locale-translate-filter-form .form-item-translation,
[dir="rtl"] .locale-translate-filter-form .form-item-customized {
float: right;
margin-left: 1em;
margin-right: 0;
}
.locale-translate-filter-form .form-type-select select {
width: 100%;
}
.locale-translate-filter-form .form-actions {
float: left; /* LTR */
padding: 3.8ex 0 0 0; /* LTR */
}
[dir="rtl"] .locale-translate-filter-form .form-actions {
float: right;
padding: 3.5ex 0 0 0;
}
.locale-translate-edit-form th {
width: 50%;
table-layout: fixed;
}
.locale-translate-edit-form td {
vertical-align: top
}
.locale-translate-edit-form tr.changed {
background: #ffb;
}
.locale-translate-edit-form tr .form-type-item .ajax-changed {
position: absolute;
}
.locale-translate-filter-form .form-wrapper {
margin-bottom:0;
}
.locale-translate-edit-form table.changed {
margin-top: 0;
}
/**
* Available translation updates page.
*/
#locale-translation-status-form table {
table-layout: fixed;
}
#locale-translation-status-form th.select-all {
width: 4%;
}
#locale-translation-status-form th.title {
width: 25%;
}
#locale-translation-status-form th.description {
}
#locale-translation-status-form td {
vertical-align: top;
}
.locale-translation-update__wrapper {
background: transparent url(../../../misc/menu-collapsed.png) left .6em no-repeat;
margin-left: -12px;
padding-left: 12px;
}
.expanded .locale-translation-update__wrapper {
background: transparent url(../../../misc/menu-expanded.png) left .6em no-repeat;
}
#locale-translation-status-form .label {
color: #1d1d1d;
font-size: 1.15em;
font-weight: bold;
}
#locale-translation-status-form .description {
cursor: pointer;
}
.locale-translation-update__wrapper {
color: #5c5c5b;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expanded .locale-translation-update__wrapper {
height: auto;
overflow: visible;
white-space: normal;
}
.expanded .locale-translation-update__message {
-webkit-hyphens: auto;
-moz-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto;
}
.js .locale-translation-update__wrapper {
height: 20px;
}
.expanded .locale-translation-update__wrapper {
height: auto;
overflow: visible;
white-space: normal;
}
.locale-translation-update__details {
padding: 5px 0;
max-width: 490px;
white-space: normal;
font-size: 0.9em;
color: #666;
}
@media screen and (max-width: 40em) {
#locale-translation-status-form th.title {
width: 20%;
}
#locale-translation-status-form th.status {
width: 40%;
}
}

View file

@ -0,0 +1,104 @@
/**
* @file
* Locale admin behavior.
*/
(function ($, Drupal) {
"use strict";
/**
* Marks changes of translations.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.localeTranslateDirty = {
attach: function () {
var $form = $("#locale-translate-edit-form").once('localetranslatedirty');
if ($form.length) {
// Display a notice if any row changed.
$form.one('formUpdated.localeTranslateDirty', 'table', function () {
var $marker = $(Drupal.theme('localeTranslateChangedWarning')).hide();
$(this).addClass('changed').before($marker);
$marker.fadeIn('slow');
});
// Highlight changed row.
$form.on('formUpdated.localeTranslateDirty', 'tr', function () {
var $row = $(this);
var $rowToMark = $row.once('localemark');
var marker = Drupal.theme('localeTranslateChangedMarker');
$row.addClass('changed');
// Add an asterisk only once if row changed.
if ($rowToMark.length) {
$rowToMark.find('td:first-child .form-item').append(marker);
}
});
}
},
detach: function (context, settings, trigger) {
if (trigger === 'unload') {
var $form = $("#locale-translate-edit-form").removeOnce('localetranslatedirty');
if ($form.length) {
$form.off('formUpdated.localeTranslateDirty');
}
}
}
};
/**
* Show/hide the description details on Available translation updates page.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.hideUpdateInformation = {
attach: function (context, settings) {
var $table = $('#locale-translation-status-form').once('expand-updates');
if ($table.length) {
var $tbodies = $table.find('tbody');
// Open/close the description details by toggling a tr class.
$tbodies.on('click keydown', '.description', function (e) {
if (e.keyCode && (e.keyCode !== 13 && e.keyCode !== 32)) {
return;
}
e.preventDefault();
var $tr = $(this).closest('tr');
$tr.toggleClass('expanded');
// Change screen reader text.
$tr.find('.locale-translation-update__prefix').text(function () {
if ($tr.hasClass('expanded')) {
return Drupal.t('Hide description');
}
else {
return Drupal.t('Show description');
}
});
});
$table.find('.requirements, .links').hide();
}
}
};
$.extend(Drupal.theme, /** @lends Drupal.theme */{
/**
*
* @return {string}
*/
localeTranslateChangedMarker: function () {
return '<abbr class="warning ajax-changed" title="' + Drupal.t('Changed') + '">*</abbr>';
},
/**
*
* @return {string}
*/
localeTranslateChangedWarning: function () {
return '<div class="clearfix messages messages--warning">' + Drupal.theme('localeTranslateChangedMarker') + ' ' + Drupal.t('Changes made in this table will not be saved until the form is submitted.') + '</div>';
}
});
})(jQuery, Drupal);

View file

@ -0,0 +1,128 @@
<?php
/**
* @file
* Hooks provided by the Locale module.
*/
/**
* @defgroup interface_translation_properties Interface translation properties
* @{
* .info.yml file properties for interface translation settings.
*
* For modules hosted on drupal.org, a project definition is automatically added
* to the .info.yml file. Only modules with this project definition are
* discovered by the update module and use it to check for new releases. Locale
* module uses the same data to build a list of modules to check for new
* translations. Therefore modules not hosted at drupal.org, such as custom
* modules, custom themes, features and distributions, need a way to identify
* themselves to the Locale module if they have translations that require to be
* updated.
*
* Custom modules which contain new strings should provide po file(s) containing
* source strings and string translations in gettext format. The translation
* file can be located both local and remote. Use the following .info.yml file
* properties to inform Locale module to load and import the translations.
*
* Example .info.yml file properties for a custom module with a po file located
* in the module's folder.
* @code
* interface translation project = example_module
* interface translation server pattern = modules/custom/example_module/%project-%version.%language.po
* @endcode
*
* Streamwrappers can be used in the server pattern definition. The interface
* translations directory (Configuration > Media > File system) can be addressed
* using the "translations://" streamwrapper. But also other streamwrappers can
* be used.
* @code
* interface translation server pattern = translations://%project-%version.%language.po
* @endcode
* @code
* interface translation server pattern = public://translations/%project-%version.%language.po
* @endcode
*
* Multiple custom modules or themes sharing the same po file should have
* matching definitions. Such as modules and sub-modules or multiple modules in
* the same project/code tree. Both "interface translation project" and
* "interface translation server pattern" definitions of these modules should
* match.
*
* Example .info.yml file properties for a custom module with a po file located
* on a remote translation server.
* @code
* interface translation project = example_module
* interface translation server pattern = http://example.com/files/translations/%core/%project/%project-%version.%language.po
* @endcode
*
* Custom themes, features and distributions can implement these .info.yml file
* properties in their .info.yml file too.
*
* To change the interface translation settings of modules and themes hosted at
* drupal.org use hook_locale_translation_projects_alter(). Possible changes
* include changing the po file location (server pattern) or removing the
* project from the translation update list.
*
* Available .info.yml file properties:
* - "interface translation project": project name. Required.
* Name of the project a (sub-)module belongs to. Multiple modules sharing
* the same project name will be listed as one the translation status list.
* - "interface translation server pattern": URL of the .po translation files
* used to download the files from. The URL contains tokens which will be
* replaced by appropriate values. The file can be locate both at a local
* relative path, a local absolute path and a remote server location.
*
* The following tokens are available for the server pattern:
* - "%core": Core version. Value example: "8.x".
* - "%project": Project name. Value examples: "drupal", "media_gallery".
* - "%version": Project version release. Value examples: "8.1", "8.x-1.0".
* - "%language": Language code. Value examples: "fr", "pt-pt".
*
* @see i18n
* @}
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter the list of projects to be updated by locale's interface translation.
*
* Locale module attempts to update the translation of those modules returned
* by \Drupal\Update\UpdateManager::getProjects(). Using this hook, the data
* returned by \Drupal\Update\UpdateManager::getProjects() can be altered or
* extended.
*
* Modules or distributions that use a dedicated translation server should use
* this hook to specify the interface translation server pattern, or to add
* additional custom/non-Drupal.org modules to the list of modules known to
* locale.
* - "interface translation server pattern": URL of the .po translation files
* used to download the files from. The URL contains tokens which will be
* replaced by appropriate values.
* The following tokens are available for the server pattern:
* - "%core": Core version. Value example: "8.x".
* - "%project": Project name. Value examples: "drupal", "media_gallery".
* - "%version": Project version release. Value examples: "8.1", "8.x-1.0".
* - "%language": Language code. Value examples: "fr", "pt-pt".
*
* @param array $projects
* Project data as returned by \Drupal\Update\UpdateManager::getProjects().
*
* @see locale_translation_project_list()
* @ingroup interface_translation_properties
*/
function hook_locale_translation_projects_alter(&$projects) {
// The translations are located at a custom translation sever.
$projects['existing_project'] = array(
'info' => array(
'interface translation server pattern' => 'http://example.com/files/translations/%core/%project/%project-%version.%language.po',
),
);
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,295 @@
<?php
/**
* @file
* Batch process to check the availability of remote or local po files.
*/
use GuzzleHttp\Exception\RequestException;
/**
* Load the common translation API.
*/
// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue: https://www.drupal.org/node/1834298.
require_once __DIR__ . '/locale.translation.inc';
/**
* Implements callback_batch_operation().
*
* Checks the presence and creation time po translation files in located at
* remote server location and local file system.
*
* @param string $project
* Machine name of the project for which to check the translation status.
* @param string $langcode
* Language code of the language for which to check the translation.
* @param array $options
* An array with options that can have the following elements:
* - 'finish_feedback': Whether or not to give feedback to the user when the
* batch is finished. Optional, defaults to TRUE.
* - 'use_remote': Whether or not to check the remote translation file.
* Optional, defaults to TRUE.
* @param array $context
* The batch context.
*/
function locale_translation_batch_status_check($project, $langcode, array $options, array &$context) {
$failure = $checked = FALSE;
$options += array(
'finish_feedback' => TRUE,
'use_remote' => TRUE,
);
$source = locale_translation_get_status(array($project), array($langcode));
$source = $source[$project][$langcode];
// Check the status of local translation files.
if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
if ($file = locale_translation_source_check_file($source)) {
locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
}
$checked = TRUE;
}
// Check the status of remote translation files.
if ($options['use_remote'] && isset($source->files[LOCALE_TRANSLATION_REMOTE])) {
$remote_file = $source->files[LOCALE_TRANSLATION_REMOTE];
if ($result = locale_translation_http_check($remote_file->uri)) {
// Update the file object with the result data. In case of a redirect we
// store the resulting uri.
if (isset($result['last_modified'])) {
$remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri;
$remote_file->timestamp = $result['last_modified'];
locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_REMOTE, $remote_file);
}
// @todo What to do with when the file is not found (404)? To prevent
// re-checking within the TTL (1day, 1week) we can set a last_checked
// timestamp or cache the result.
$checked = TRUE;
}
else {
$failure = TRUE;
}
}
// Provide user feedback and record success or failure for reporting at the
// end of the batch.
if ($options['finish_feedback'] && $checked) {
$context['results']['files'][] = $source->name;
}
if ($failure && !$checked) {
$context['results']['failed_files'][] = $source->name;
}
$context['message'] = t('Checked translation for %project.', array('%project' => $source->project));
}
/**
* Implements callback_batch_finished().
*
* Set result message.
*
* @param bool $success
* TRUE if batch successfully completed.
* @param array $results
* Batch results.
*/
function locale_translation_batch_status_finished($success, $results) {
if ($success) {
if (isset($results['failed_files'])) {
if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
$message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation file could not be checked. <a href="@url">See the log</a> for details.', '@count translation files could not be checked. <a href="@url">See the log</a> for details.', array('@url' => \Drupal::url('dblog.overview')));
}
else {
$message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation files could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.');
}
drupal_set_message($message, 'error');
}
if (isset($results['files'])) {
drupal_set_message(\Drupal::translation()->formatPlural(
count($results['files']),
'Checked available interface translation updates for one project.',
'Checked available interface translation updates for @count projects.'
));
}
if (!isset($results['failed_files']) && !isset($results['files'])) {
drupal_set_message(t('Nothing to check.'));
}
\Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
}
else {
drupal_set_message(t('An error occurred trying to check available interface translation updates.'), 'error');
}
}
/**
* Implements callback_batch_operation().
*
* Downloads a remote gettext file into the translations directory. When
* successfully the translation status is updated.
*
* @param object $project
* Source object of the translatable project.
* @param string $langcode
* Language code.
* @param array $context
* The batch context.
*
* @see locale_translation_batch_fetch_import()
*/
function locale_translation_batch_fetch_download($project, $langcode, &$context) {
$sources = locale_translation_get_status(array($project), array($langcode));
if (isset($sources[$project][$langcode])) {
$source = $sources[$project][$langcode];
if (isset($source->type) && $source->type == LOCALE_TRANSLATION_REMOTE) {
if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE], 'translations://')) {
$context['message'] = t('Downloaded translation for %project.', array('%project' => $source->project));
locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
}
else {
$context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE];
}
}
}
}
/**
* Implements callback_batch_operation().
*
* Imports a gettext file from the translation directory. When successfully the
* translation status is updated.
*
* @param object $project
* Source object of the translatable project.
* @param string $langcode
* Language code.
* @param array $options
* Array of import options.
* @param array $context
* The batch context.
*
* @see locale_translate_batch_import_files()
* @see locale_translation_batch_fetch_download()
*/
function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) {
$sources = locale_translation_get_status(array($project), array($langcode));
if (isset($sources[$project][$langcode])) {
$source = $sources[$project][$langcode];
if (isset($source->type)) {
if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) {
$file = $source->files[LOCALE_TRANSLATION_LOCAL];
module_load_include('bulk.inc', 'locale');
$options += array(
'message' => t('Importing translation for %project.', array('%project' => $source->project)),
);
// Import the translation file. For large files the batch operations is
// progressive and will be called repeatedly until finished.
locale_translate_batch_import($file, $options, $context);
// The import is finished.
if (isset($context['finished']) && $context['finished'] == 1) {
// The import is successful.
if (isset($context['results']['files'][$file->uri])) {
$context['message'] = t('Imported translation for %project.', array('%project' => $source->project));
// Save the data of imported source into the {locale_file} table and
// update the current translation status.
locale_translation_status_save($project, $langcode, LOCALE_TRANSLATION_CURRENT, $source->files[LOCALE_TRANSLATION_LOCAL]);
}
}
}
}
}
}
/**
* Implements callback_batch_finished().
*
* Set result message.
*
* @param bool $success
* TRUE if batch successfully completed.
* @param array $results
* Batch results.
*/
function locale_translation_batch_fetch_finished($success, $results) {
module_load_include('bulk.inc', 'locale');
if ($success) {
\Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
}
return locale_translate_batch_finished($success, $results);
}
/**
* Check if remote file exists and when it was last updated.
*
* @param string $uri
* URI of remote file.
*
* @return array|bool
* Associative array of file data with the following elements:
* - last_modified: Last modified timestamp of the translation file.
* - (optional) location: The location of the translation file. Is only set
* when a redirect (301) has occurred.
* TRUE if the file is not found. FALSE if a fault occurred.
*/
function locale_translation_http_check($uri) {
$logger = \Drupal::logger('locale');
try {
$response = \Drupal::httpClient()->head($uri);
$result = array();
// Return the effective URL if it differs from the requested.
if ($response->getEffectiveUrl() != $uri) {
$result['location'] = $response->getEffectiveUrl();
}
$result['last_modified'] = $response->hasHeader('Last-Modified') ? strtotime($response->getHeader('Last-Modified')) : 0;
return $result;
}
catch (RequestException $e) {
// Handle 4xx and 5xx http responses.
if ($response = $e->getResponse()) {
if ($response->getStatusCode() == 404) {
// File not found occurs when a translation file is not yet available
// at the translation server. But also if a custom module or custom
// theme does not define the location of a translation file. By default
// the file is checked at the translation server, but it will not be
// found there.
$logger->notice('Translation file not found: @uri.', array('@uri' => $uri));
return TRUE;
}
$logger->notice('HTTP request to @url failed with error: @error.', array('@url' => $uri, '@error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()));
}
}
return FALSE;
}
/**
* Downloads a translation file from a remote server.
*
* @param object $source_file
* Source file object with at least:
* - "uri": uri to download the file from.
* - "project": Project name.
* - "langcode": Translation language.
* - "version": Project version.
* - "filename": File name.
* @param string $directory
* Directory where the downloaded file will be saved. Defaults to the
* temporary file path.
*
* @return object
* File object if download was successful. FALSE on failure.
*/
function locale_translation_download_source($source_file, $directory = 'temporary://') {
if ($uri = system_retrieve_file($source_file->uri, $directory)) {
$file = clone($source_file);
$file->type = LOCALE_TRANSLATION_LOCAL;
$file->uri = $uri;
$file->directory = $directory;
$file->timestamp = filemtime($uri);
return $file;
}
\Drupal::logger('locale')->error('Unable to download translation file @uri.', array('@uri' => $source_file->uri));
return FALSE;
}

View file

@ -0,0 +1,641 @@
<?php
/**
* @file
* Mass import-export and batch import functionality for Gettext .po files.
*/
use Drupal\Core\Language\LanguageInterface;
use Drupal\file\FileInterface;
use Drupal\locale\Gettext;
use Drupal\locale\Locale;
/**
* Prepare a batch to import all translations.
*
* @param array $options
* An array with options that can have the following elements:
* - 'langcode': The language code. Optional, defaults to NULL, which means
* that the language will be detected from the name of the files.
* - 'overwrite_options': Overwrite options array as defined in
* Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
* are customized translations or come from a community source. Use
* LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
* LOCALE_NOT_CUSTOMIZED.
* - 'finish_feedback': Whether or not to give feedback to the user when the
* batch is finished. Optional, defaults to TRUE.
* @param bool $force
* (optional) Import all available files, even if they were imported before.
*
* @return array|bool
* The batch structure, or FALSE if no files are used to build the batch.
*
* @todo
* Integrate with update status to identify projects needed and integrate
* l10n_update functionality to feed in translation files alike.
* See https://www.drupal.org/node/1191488.
*/
function locale_translate_batch_import_files(array $options, $force = FALSE) {
$options += array(
'overwrite_options' => array(),
'customized' => LOCALE_NOT_CUSTOMIZED,
'finish_feedback' => TRUE,
);
if (!empty($options['langcode'])) {
$langcodes = array($options['langcode']);
}
else {
// If langcode was not provided, make sure to only import files for the
// languages we have added.
$langcodes = array_keys(\Drupal::languageManager()->getLanguages());
}
$files = locale_translate_get_interface_translation_files(array(), $langcodes);
if (!$force) {
$result = db_select('locale_file', 'lf')
->fields('lf', array('langcode', 'uri', 'timestamp'))
->condition('langcode', $langcodes)
->execute()
->fetchAllAssoc('uri');
foreach ($result as $uri => $info) {
if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) {
// The file is already imported and not changed since the last import.
// Remove it from file list and don't import it again.
unset($files[$uri]);
}
}
}
return locale_translate_batch_build($files, $options);
}
/**
* Get interface translation files present in the translations directory.
*
* @param array $projects
* (optional) Project names from which to get the translation files and
* history. Defaults to all projects.
* @param array $langcodes
* (optional) Language codes from which to get the translation files and
* history. Defaults to all languages.
*
* @return array
* An array of interface translation files keyed by their URI.
*/
function locale_translate_get_interface_translation_files(array $projects = array(), array $langcodes = array()) {
module_load_include('compare.inc', 'locale');
$files = array();
$projects = $projects ? $projects : array_keys(locale_translation_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
// Scan the translations directory for files matching a name pattern
// containing a project name and language code: {project}.{langcode}.po or
// {project}-{version}.{langcode}.po.
// Only files of known projects and languages will be returned.
$directory = \Drupal::config('locale.settings')->get('translation.path');
$result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', array('recurse' => FALSE));
foreach ($result as $file) {
// Update the file object with project name and version from the file name.
$file = locale_translate_file_attach_properties($file);
if (in_array($file->project, $projects)) {
if (in_array($file->langcode, $langcodes)) {
$files[$file->uri] = $file;
}
}
}
return $files;
}
/**
* Build a locale batch from an array of files.
*
* @param array $files
* Array of file objects to import.
* @param array $options
* An array with options that can have the following elements:
* - 'langcode': The language code. Optional, defaults to NULL, which means
* that the language will be detected from the name of the files.
* - 'overwrite_options': Overwrite options array as defined in
* Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
* are customized translations or come from a community source. Use
* LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
* LOCALE_NOT_CUSTOMIZED.
* - 'finish_feedback': Whether or not to give feedback to the user when the
* batch is finished. Optional, defaults to TRUE.
*
* @return array|bool
* A batch structure or FALSE if $files was empty.
*/
function locale_translate_batch_build(array $files, array $options) {
$options += array(
'overwrite_options' => array(),
'customized' => LOCALE_NOT_CUSTOMIZED,
'finish_feedback' => TRUE,
);
if (count($files)) {
$operations = array();
foreach ($files as $file) {
// We call locale_translate_batch_import for every batch operation.
$operations[] = array('locale_translate_batch_import', array($file, $options));
}
// Save the translation status of all files.
$operations[] = array('locale_translate_batch_import_save', array());
// Add a final step to refresh JavaScript and configuration strings.
$operations[] = array('locale_translate_batch_refresh', array());
$batch = array(
'operations' => $operations,
'title' => t('Importing interface translations'),
'progress_message' => '',
'error_message' => t('Error importing interface translations'),
'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
);
if ($options['finish_feedback']) {
$batch['finished'] = 'locale_translate_batch_finished';
}
return $batch;
}
return FALSE;
}
/**
* Implements callback_batch_operation().
*
* Perform interface translation import.
*
* @param object $file
* A file object of the gettext file to be imported. The file object must
* contain a language parameter (other than
* LanguageInterface::LANGCODE_NOT_SPECIFIED). This is used as the language of
* the import.
* @param array $options
* An array with options that can have the following elements:
* - 'langcode': The language code.
* - 'overwrite_options': Overwrite options array as defined in
* Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
* are customized translations or come from a community source. Use
* LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
* LOCALE_NOT_CUSTOMIZED.
* - 'message': Alternative message to display during import. Note, this must
* be sanitized text.
* @param array $context
* Contains a list of files imported.
*/
function locale_translate_batch_import($file, array $options, array &$context) {
// Merge the default values in the $options array.
$options += array(
'overwrite_options' => array(),
'customized' => LOCALE_NOT_CUSTOMIZED,
);
if (isset($file->langcode) && $file->langcode != LanguageInterface::LANGCODE_NOT_SPECIFIED) {
try {
if (empty($context['sandbox'])) {
$context['sandbox']['parse_state'] = array(
'filesize' => filesize(drupal_realpath($file->uri)),
'chunk_size' => 200,
'seek' => 0,
);
}
// Update the seek and the number of items in the $options array().
$options['seek'] = $context['sandbox']['parse_state']['seek'];
$options['items'] = $context['sandbox']['parse_state']['chunk_size'];
$report = GetText::fileToDatabase($file, $options);
// If not yet finished with reading, mark progress based on size and
// position.
if ($report['seek'] < filesize($file->uri)) {
$context['sandbox']['parse_state']['seek'] = $report['seek'];
// Maximize the progress bar at 95% before completion, the batch API
// could trigger the end of the operation before file reading is done,
// because of floating point inaccuracies. See
// https://www.drupal.org/node/1089472.
$context['finished'] = min(0.95, $report['seek'] / filesize($file->uri));
if (isset($options['message'])) {
$context['message'] = t('!message (@percent%).', array('!message' => $options['message'], '@percent' => (int) ($context['finished'] * 100)));
}
else {
$context['message'] = t('Importing translation file: %filename (@percent%).', array('%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100)));
}
}
else {
// We are finished here.
$context['finished'] = 1;
// Store the file data for processing by the next batch operation.
$file->timestamp = filemtime($file->uri);
$context['results']['files'][$file->uri] = $file;
$context['results']['languages'][$file->uri] = $file->langcode;
}
// Add the reported values to the statistics for this file.
// Each import iteration reports statistics in an array. The results of
// each iteration are added and merged here and stored per file.
if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) {
$context['results']['stats'][$file->uri] = array();
}
foreach ($report as $key => $value) {
if (is_numeric($report[$key])) {
if (!isset($context['results']['stats'][$file->uri][$key])) {
$context['results']['stats'][$file->uri][$key] = 0;
}
$context['results']['stats'][$file->uri][$key] += $report[$key];
}
elseif (is_array($value)) {
$context['results']['stats'][$file->uri] += array($key => array());
$context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value);
}
}
}
catch (Exception $exception) {
// Import failed. Store the data of the failing file.
$context['results']['failed_files'][] = $file;
\Drupal::logger('locale')->notice('Unable to import translations file: @file', array('@file' => $file->uri));
}
}
}
/**
* Implements callback_batch_operation().
*
* Save data of imported files.
*
* @param array $context
* Contains a list of imported files.
*/
function locale_translate_batch_import_save(array $context) {
if (isset($context['results']['files'])) {
foreach ($context['results']['files'] as $file) {
// Update the file history if both project and version are known. This
// table is used by the automated translation update function which tracks
// translation status of module and themes in the system. Other
// translation files are not tracked and are therefore not stored in this
// table.
if ($file->project && $file->version) {
$file->last_checked = REQUEST_TIME;
locale_translation_update_file_history($file);
}
}
$context['message'] = t('Translations imported.');
}
}
/**
* Implements callback_batch_operation().
*
* Refreshes translations after importing strings.
*
* @param array $context
* Contains a list of strings updated and information about the progress.
*/
function locale_translate_batch_refresh(array &$context) {
if (!isset($context['sandbox']['refresh'])) {
$strings = $langcodes = array();
if (isset($context['results']['stats'])) {
// Get list of unique string identifiers and language codes updated.
$langcodes = array_unique(array_values($context['results']['languages']));
foreach ($context['results']['stats'] as $report) {
$strings = array_merge($strings, $report['strings']);
}
}
if ($strings) {
// Initialize multi-step string refresh.
$context['message'] = t('Updating translations for JavaScript and default configuration.');
$context['sandbox']['refresh']['strings'] = array_unique($strings);
$context['sandbox']['refresh']['languages'] = $langcodes;
$context['sandbox']['refresh']['names'] = array();
$context['results']['stats']['config'] = 0;
$context['sandbox']['refresh']['count'] = count($strings);
// We will update strings on later steps.
$context['finished'] = 0;
}
else {
$context['finished'] = 1;
}
}
elseif ($name = array_shift($context['sandbox']['refresh']['names'])) {
// Refresh all languages for one object at a time.
$count = Locale::config()->updateConfigTranslations(array($name), $context['sandbox']['refresh']['languages']);
$context['results']['stats']['config'] += $count;
// Inherit finished information from the "parent" string lookup step so
// visual display of status will make sense.
$context['finished'] = $context['sandbox']['refresh']['names_finished'];
$context['message'] = t('Updating default configuration (@percent%).', array('@percent' => (int) ($context['finished'] * 100)));
}
elseif (!empty($context['sandbox']['refresh']['strings'])) {
// Not perfect but will give some indication of progress.
$context['finished'] = 1 - count($context['sandbox']['refresh']['strings']) / $context['sandbox']['refresh']['count'];
// Pending strings, refresh 100 at a time, get next pack.
$next = array_slice($context['sandbox']['refresh']['strings'], 0, 100);
array_splice($context['sandbox']['refresh']['strings'], 0, count($next));
// Clear cache and force refresh of JavaScript translations.
_locale_refresh_translations($context['sandbox']['refresh']['languages'], $next);
// Check whether we need to refresh configuration objects.
if ($names = \Drupal\locale\Locale::config()->getStringNames($next)) {
$context['sandbox']['refresh']['names_finished'] = $context['finished'];
$context['sandbox']['refresh']['names'] = $names;
}
}
else {
$context['message'] = t('Updated default configuration.');
$context['finished'] = 1;
}
}
/**
* Implements callback_batch_finished().
*
* Finished callback of system page locale import batch.
*
* @param bool $success
* TRUE if batch successfully completed.
* @param array $results
* Batch results.
*/
function locale_translate_batch_finished($success, array $results) {
$logger = \Drupal::logger('locale');
if ($success) {
$additions = $updates = $deletes = $skips = $config = 0;
if (isset($results['failed_files'])) {
if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
$message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation file could not be imported. <a href="@url">See the log</a> for details.', '@count translation files could not be imported. <a href="@url">See the log</a> for details.', array('@url' => \Drupal::url('dblog.overview')));
}
else {
$message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation file could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.');
}
drupal_set_message($message, 'error');
}
if (isset($results['files'])) {
$skipped_files = array();
// If there are no results and/or no stats (eg. coping with an empty .po
// file), simply do nothing.
if ($results && isset($results['stats'])) {
foreach ($results['stats'] as $filepath => $report) {
$additions += $report['additions'];
$updates += $report['updates'];
$deletes += $report['deletes'];
$skips += $report['skips'];
if ($report['skips'] > 0) {
$skipped_files[] = $filepath;
}
}
}
drupal_set_message(\Drupal::translation()->formatPlural(count($results['files']),
'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
'@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.',
array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)
));
$logger->notice('Translations imported: %number added, %update updated, %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes));
if ($skips) {
if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
$message = \Drupal::translation()->formatPlural($skips, 'One translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => \Drupal::url('dblog.overview')));
}
else {
$message = \Drupal::translation()->formatPlural($skips, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.');
}
drupal_set_message($message, 'warning');
$logger->warning('@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)));
}
}
}
// Add messages for configuration too.
if (isset($results['stats']['config'])) {
locale_config_batch_finished($success, $results);
}
}
/**
* Creates a file object and populates the timestamp property.
*
* @param string $filepath
* The filepath of a file to import.
*
* @return object
* An object representing the file.
*/
function locale_translate_file_create($filepath) {
$file = new stdClass();
$file->filename = drupal_basename($filepath);
$file->uri = $filepath;
$file->timestamp = filemtime($file->uri);
return $file;
}
/**
* Generates file properties from filename and options.
*
* An attempt is made to determine the translation language, project name and
* project version from the file name. Supported file name patterns are:
* {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po.
* Alternatively the translation language can be set using the $options.
*
* @param object $file
* A file object of the gettext file to be imported.
* @param array $options
* An array with options:
* - 'langcode': The language code. Overrides the file language.
*
* @return object
* Modified file object.
*/
function locale_translate_file_attach_properties($file, array $options = array()) {
// If $file is a file entity, convert it to a stdClass.
if ($file instanceof FileInterface) {
$file = (object) array(
'filename' => $file->getFilename(),
'uri' => $file->getFileUri(),
);
}
// Extract project, version and language code from the file name. Supported:
// {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po
preg_match('!
( # project OR project and version OR empty (group 1)
([a-z_]+) # project name (group 2)
\. # .
| # OR
([a-z_]+) # project name (group 3)
\- # -
([0-9a-z\.\-\+]+) # version (group 4)
\. # .
| # OR
) # (empty)
([^\./]+) # language code (group 5)
\. # .
po # po extension
$!x', $file->filename, $matches);
if (isset($matches[5])) {
$file->project = $matches[2] . $matches[3];
$file->version = $matches[4];
$file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5];
}
else {
$file->langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
}
return $file;
}
/**
* Deletes interface translation files and translation history records.
*
* @param array $projects
* (optional) Project names from which to delete the translation files and
* history. Defaults to all projects.
* @param array $langcodes
* (optional) Language codes from which to delete the translation files and
* history. Defaults to all languages.
*
* @return bool
* TRUE if files are removed successfully. FALSE if one or more files could
* not be deleted.
*/
function locale_translate_delete_translation_files(array $projects = array(), array $langcodes = array()) {
$fail = FALSE;
locale_translation_file_history_delete($projects, $langcodes);
// Delete all translation files from the translations directory.
if ($files = locale_translate_get_interface_translation_files($projects, $langcodes)) {
foreach ($files as $file) {
$success = file_unmanaged_delete($file->uri);
if (!$success) {
$fail = TRUE;
}
}
}
return !$fail;
}
/**
* Builds a locale batch to refresh configuration.
*
* @param array $options
* An array with options that can have the following elements:
* - 'finish_feedback': (optional) Whether or not to give feedback to the user
* when the batch is finished. Defaults to TRUE.
* @param array $langcodes
* (optional) Array of language codes. Defaults to all translatable languages.
* @param array $components
* (optional) Array of component lists indexed by type. If not present or it
* is an empty array, it will update all components.
*
* @return array
* The batch definition.
*/
function locale_config_batch_update_components(array $options, array $langcodes = array(), array $components = array()) {
$langcodes = $langcodes ? $langcodes : array_keys(\Drupal::languageManager()->getLanguages());
if ($langcodes && $names = \Drupal\locale\Locale::config()->getComponentNames($components)) {
return locale_config_batch_build($names, $langcodes, $options);
}
}
/**
* Creates a locale batch to refresh specific configuration.
*
* @param array $names
* List of configuration object names (which are strings) to update.
* @param array $langcodes
* List of language codes to refresh.
* @param array $options
* (optional) An array with options that can have the following elements:
* - 'finish_feedback': Whether or not to give feedback to the user when the
* batch is finished. Defaults to TRUE.
*
* @return array
* The batch definition.
*
* @see locale_config_batch_refresh_name()
*/
function locale_config_batch_build(array $names, array $langcodes, array $options = array()) {
$options += array('finish_feedback' => TRUE);
$i = 0;
$batch_names = array();
$operations = array();
foreach ($names as $name) {
$batch_names[] = $name;
$i++;
// During installation the caching of configuration objects is disabled so
// it is very expensive to initialize the \Drupal::config() object on each
// request. We batch a small number of configuration object upgrades
// together to improve the overall performance of the process.
if ($i % 20 == 0) {
$operations[] = array('locale_config_batch_refresh_name', array($batch_names, $langcodes));
$batch_names = array();
}
}
if (!empty($batch_names)) {
$operations[] = array('locale_config_batch_refresh_name', array($batch_names, $langcodes));
}
$batch = array(
'operations' => $operations,
'title' => t('Updating configuration translations'),
'init_message' => t('Starting configuration update'),
'error_message' => t('Error updating configuration translations'),
'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
);
if (!empty($options['finish_feedback'])) {
$batch['completed'] = 'locale_config_batch_finished';
}
return $batch;
}
/**
* Implements callback_batch_operation().
*
* Performs configuration translation refresh.
*
* @param array $names
* An array of names of configuration objects to update.
* @param array $langcodes
* (optional) Array of language codes to update. Defaults to all languages.
* @param array $context
* Contains a list of files imported.
*
* @see locale_config_batch_build()
*/
function locale_config_batch_refresh_name(array $names, array $langcodes, array &$context) {
if (!isset($context['result']['stats']['config'])) {
$context['result']['stats']['config'] = 0;
}
$context['result']['stats']['config'] += Locale::config()->updateConfigTranslations($names, $langcodes);
foreach ($names as $name) {
$context['result']['names'][] = $name;
}
$context['result']['langcodes'] = $langcodes;
$context['finished'] = 1;
}
/**
* Implements callback_batch_finished().
*
* Finishes callback of system page locale import batch.
*
* @param bool $success
* Information about the success of the batch import.
* @param array $results
* Information about the results of the batch import.
*
* @see locale_config_batch_build()
*/
function locale_config_batch_finished($success, array $results) {
if ($success) {
$configuration = isset($results['stats']['config']) ? $results['stats']['config'] : 0;
if ($configuration) {
drupal_set_message(t('The configuration was successfully updated. There are %number configuration objects updated.', array('%number' => $configuration)));
\Drupal::logger('locale')->notice('The configuration was successfully updated. %number configuration objects updated.', array('%number' => $configuration));
}
else {
drupal_set_message(t('No configuration objects have been updated.'));
\Drupal::logger('locale')->warning('No configuration objects have been updated.');
}
}
}

View file

@ -0,0 +1,35 @@
/**
* @file
* Locale behavior.
*/
(function ($, Drupal) {
"use strict";
/**
* Select the language code of an imported file based on its filename.
*
* This only works if the file name ends with "LANGCODE.po".
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.importLanguageCodeSelector = {
attach: function (context, settings) {
var $form = $('#locale-translate-import-form').once('autodetect-lang');
if ($form.length) {
var $langcode = $form.find('.langcode-input');
$form.find('.file-import-input')
.on('change', function () {
// If the filename is fully the language code or the filename
// ends with a language code, pre-select that one.
var matches = $(this).val().match(/([^.][\.]*)([\w-]+)\.po$/);
if (matches && $langcode.find('option[value="' + matches[2] + '"]').length) {
$langcode.val(matches[2]);
}
});
}
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,334 @@
<?php
/**
* @file
* The API for comparing project translation status with available translation.
*/
use Drupal\Core\Cache;
use Drupal\Core\Utility\ProjectInfo;
/**
* Load common APIs.
*/
// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue: https://www.drupal.org/node/1834298.
require_once __DIR__ . '/locale.translation.inc';
/**
* Clear the project data table.
*/
function locale_translation_flush_projects() {
\Drupal::service('locale.project')->deleteAll();
}
/**
* Builds list of projects and stores the result in the database.
*
* The project data is based on the project list supplied by the Update module.
* Only the properties required by Locale module is included and additional
* (custom) modules and translation server data is added.
*
* In case the Update module is disabled this function will return an empty
* array.
*
* @return array
* Array of project data:
* - "name": Project system name.
* - "project_type": Project type, e.g. 'module', 'theme'.
* - "core": Core release version, e.g. 8.x
* - "version": Project release version, e.g. 8.x-1.0
* See http://drupalcode.org/project/drupalorg.git/blob/refs/heads/7.x-3.x:/drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php#l219
* for how the version strings are created.
* - "server_pattern": Translation server po file pattern.
* - "status": Project status, 1 = enabled.
*/
function locale_translation_build_projects() {
// This function depends on Update module. We degrade gracefully.
if (!\Drupal::moduleHandler()->moduleExists('update')) {
return array();
}
// Get the project list based on .info.yml files.
$projects = locale_translation_project_list();
// Mark all previous projects as disabled and store new project data.
\Drupal::service('locale.project')->disableAll();
$default_server = locale_translation_default_translation_server();
// If project is a dev release, or core, find the latest available release.
$project_updates = update_get_available(TRUE);
foreach ($projects as $name => $data) {
if (isset($project_updates[$name]['releases']) && $project_updates[$name]['project_status'] != 'not-fetched') {
// Find out if a dev version is installed.
if (preg_match("/^\d+\.x-(\d+)\..*-dev$/", $data['info']['version'], $matches) ||
preg_match("/^(\d+)\.\d+\.\d+.*-dev$/", $data['info']['version'], $matches)) {
// Find a suitable release to use as alternative translation.
foreach ($project_updates[$name]['releases'] as $project_release) {
// The first release with the same major release number which is not a
// dev release is the one. Releases are sorted the most recent first.
// For example the major release number for a contrib module
// 8.x-2.x-dev is "2", for core 8.1.0-dev is "8".
// @todo https://www.drupal.org/node/1774024 Make a helper function.
if ($project_release['version_major'] == $matches[1] &&
(!isset($project_release['version_extra']) || $project_release['version_extra'] != 'dev')) {
$release = $project_release;
break;
}
}
}
if (!empty($release['version'])) {
$data['info']['version'] = $release['version'];
}
unset($release);
}
// For every project store information.
$data += array(
'name' => $name,
'version' => isset($data['info']['version']) ? $data['info']['version'] : '',
'core' => isset($data['info']['core']) ? $data['info']['core'] : \Drupal::CORE_COMPATIBILITY,
// A project can provide the path and filename pattern to download the
// gettext file. Use the default if not.
'server_pattern' => isset($data['info']['interface translation server pattern']) && $data['info']['interface translation server pattern'] ? $data['info']['interface translation server pattern'] : $default_server['pattern'],
'status' => !empty($data['project_status']) ? 1 : 0,
);
$project = (object) $data;
$projects[$name] = $project;
// Create or update the project record.
\Drupal::service('locale.project')->set($project->name, $data);
// Invalidate the cache of translatable projects.
locale_translation_clear_cache_projects();
}
return $projects;
}
/**
* Fetch an array of projects for translation update.
*
* @return array
* Array of project data including .info.yml file data.
*/
function locale_translation_project_list() {
$projects = &drupal_static(__FUNCTION__, array());
if (empty($projects)) {
module_load_include('compare.inc', 'update');
$config = \Drupal::config('locale.settings');
$projects = array();
$additional_whitelist = array(
'interface translation project',
'interface translation server pattern',
);
$module_data = _locale_translation_prepare_project_list(system_rebuild_module_data(), 'module');
$theme_data = _locale_translation_prepare_project_list(\Drupal::service('theme_handler')->rebuildThemeData(), 'theme');
$project_info = new ProjectInfo();
$project_info->processInfoList($projects, $module_data, 'module', TRUE, $additional_whitelist);
$project_info->processInfoList($projects, $theme_data, 'theme', TRUE, $additional_whitelist);
// Allow other modules to alter projects before fetching and comparing.
\Drupal::moduleHandler()->alter('locale_translation_projects', $projects);
}
return $projects;
}
/**
* Prepare module and theme data.
*
* Modify .info.yml file data before it is processed by
* \Drupal\Core\Utility\ProjectInfo->processInfoList(). In order for
* \Drupal\Core\Utility\ProjectInfo->processInfoList() to recognize a project,
* it requires the 'project' parameter in the .info.yml file data.
*
* Custom modules or themes can bring their own gettext translation file. To
* enable import of this file the module or theme defines "interface translation
* project = myproject" in its .info.yml file. This function will add a project
* "myproject" to the info data.
*
* @param \Drupal\Core\Extension\Extension[] $data
* Array of .info.yml file data.
* @param string $type
* The project type. i.e. module, theme.
*
* @return array
* Array of .info.yml file data.
*/
function _locale_translation_prepare_project_list($data, $type) {
foreach ($data as $name => $file) {
// Include interface translation projects. To allow
// \Drupal\Core\Utility\ProjectInfo->processInfoList() to identify this as
// a project the 'project' property is filled with the
// 'interface translation project' value.
if (isset($file->info['interface translation project'])) {
$data[$name]->info['project'] = $file->info['interface translation project'];
}
}
return $data;
}
/**
* Retrieve data for default server.
*
* @return array
* Array of server parameters:
* - "server_pattern": URI containing po file pattern.
*/
function locale_translation_default_translation_server() {
$pattern = \Drupal::config('locale.settings')->get('translation.default_server_pattern');
// An additional check is required here. During the upgrade process
// \Drupal::config()->get() returns NULL. We use the defined value as
// fallback.
$pattern = $pattern ? $pattern : LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN;
return array(
'pattern' => $pattern,
);
}
/**
* Check for the latest release of project translations.
*
* @param array $projects
* Array of project names to check. Defaults to all translatable projects.
* @param string $langcodes
* Array of language codes. Defaults to all translatable languages.
*
* @return array
* Available sources indexed by project and language.
*
* @todo Return batch or NULL.
*/
function locale_translation_check_projects($projects = array(), $langcodes = array()) {
if (locale_translation_use_remote_source()) {
// Retrieve the status of both remote and local translation sources by
// using a batch process.
locale_translation_check_projects_batch($projects, $langcodes);
}
else {
// Retrieve and save the status of local translations only.
locale_translation_check_projects_local($projects, $langcodes);
\Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
}
}
/**
* Gets and stores the status and timestamp of remote po files.
*
* A batch process is used to check for po files at remote locations and (when
* configured) to check for po files in the local file system. The most recent
* translation source states are stored in the state variable
* 'locale.translation_status'.
*
* @param array $projects
* Array of project names to check. Defaults to all translatable projects.
* @param string $langcodes
* Array of language codes. Defaults to all translatable languages.
*/
function locale_translation_check_projects_batch($projects = array(), $langcodes = array()) {
// Build and set the batch process.
$batch = locale_translation_batch_status_build($projects, $langcodes);
batch_set($batch);
}
/**
* Builds a batch to get the status of remote and local translation files.
*
* The batch process fetches the state of both local and (if configured) remote
* translation files. The data of the most recent translation is stored per
* per project and per language. This data is stored in a state variable
* 'locale.translation_status'. The timestamp it was last updated is stored
* in the state variable 'locale.translation_last_checked'.
*
* @param array $projects
* Array of project names for which to check the state of translation files.
* Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
*
* @return array
* Batch definition array.
*/
function locale_translation_batch_status_build($projects = array(), $langcodes = array()) {
$projects = $projects ? $projects : array_keys(locale_translation_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
$options = _locale_translation_default_update_options();
$operations = _locale_translation_batch_status_operations($projects, $langcodes, $options);
$batch = array(
'operations' => $operations,
'title' => t('Checking translations'),
'progress_message' => '',
'finished' => 'locale_translation_batch_status_finished',
'error_message' => t('Error checking translation updates.'),
'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc',
);
return $batch;
}
/**
* Helper function to construct batch operations checking remote translation
* status.
*
* @param array $projects
* Array of project names to be processed.
* @param array $langcodes
* Array of language codes.
* @param array $options
* Batch processing options.
*
* @return array
* Array of batch operations.
*/
function _locale_translation_batch_status_operations($projects, $langcodes, $options = array()) {
$operations = array();
foreach ($projects as $project) {
foreach ($langcodes as $langcode) {
// Check status of local and remote translation sources.
$operations[] = array('locale_translation_batch_status_check', array($project, $langcode, $options));
}
}
return $operations;
}
/**
* Check and store the status and timestamp of local po files.
*
* Only po files in the local file system are checked. Any remote translation
* files will be ignored.
*
* Projects may contain a server_pattern option containing a pattern of the
* path to the po source files. If no server_pattern is defined the default
* translation directory is checked for the po file. When a server_pattern is
* defined the specified location is checked. The server_pattern can be set in
* the module's .info.yml file or by using
* hook_locale_translation_projects_alter().
*
* @param array $projects
* Array of project names for which to check the state of translation files.
* Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
*/
function locale_translation_check_projects_local($projects = array(), $langcodes = array()) {
$projects = locale_translation_get_projects($projects);
$langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
// For each project and each language we check if a local po file is
// available. When found the source object is updated with the appropriate
// type and timestamp of the po file.
foreach ($projects as $name => $project) {
foreach ($langcodes as $langcode) {
$source = locale_translation_source_build($project, $langcode);
$file = locale_translation_source_check_file($source);
locale_translation_status_save($name, $langcode, LOCALE_TRANSLATION_LOCAL, $file);
}
}
}

View file

@ -0,0 +1,88 @@
/**
* @file
* Datepicker JavaScript for the Locale module.
*/
(function ($, Drupal, drupalSettings) {
"use strict";
/**
* Attaches language support to the jQuery UI datepicker component.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.localeDatepicker = {
attach: function (context, settings) {
// This code accesses drupalSettings and localized strings via Drupal.t().
// So this code should run after these are initialized. By placing it in an
// attach behavior this is assured.
$.datepicker.regional['drupal-locale'] = $.extend({
closeText: Drupal.t('Done'),
prevText: Drupal.t('Prev'),
nextText: Drupal.t('Next'),
currentText: Drupal.t('Today'),
monthNames: [
Drupal.t('January', {}, {context: "Long month name"}),
Drupal.t('February', {}, {context: "Long month name"}),
Drupal.t('March', {}, {context: "Long month name"}),
Drupal.t('April', {}, {context: "Long month name"}),
Drupal.t('May', {}, {context: "Long month name"}),
Drupal.t('June', {}, {context: "Long month name"}),
Drupal.t('July', {}, {context: "Long month name"}),
Drupal.t('August', {}, {context: "Long month name"}),
Drupal.t('September', {}, {context: "Long month name"}),
Drupal.t('October', {}, {context: "Long month name"}),
Drupal.t('November', {}, {context: "Long month name"}),
Drupal.t('December', {}, {context: "Long month name"})
],
monthNamesShort: [
Drupal.t('Jan'),
Drupal.t('Feb'),
Drupal.t('Mar'),
Drupal.t('Apr'),
Drupal.t('May'),
Drupal.t('Jun'),
Drupal.t('Jul'),
Drupal.t('Aug'),
Drupal.t('Sep'),
Drupal.t('Oct'),
Drupal.t('Nov'),
Drupal.t('Dec')
],
dayNames: [
Drupal.t('Sunday'),
Drupal.t('Monday'),
Drupal.t('Tuesday'),
Drupal.t('Wednesday'),
Drupal.t('Thursday'),
Drupal.t('Friday'),
Drupal.t('Saturday')
],
dayNamesShort: [
Drupal.t('Sun'),
Drupal.t('Mon'),
Drupal.t('Tue'),
Drupal.t('Wed'),
Drupal.t('Thu'),
Drupal.t('Fri'),
Drupal.t('Sat')
],
dayNamesMin: [
Drupal.t('Su'),
Drupal.t('Mo'),
Drupal.t('Tu'),
Drupal.t('We'),
Drupal.t('Th'),
Drupal.t('Fr'),
Drupal.t('Sa')
],
dateFormat: Drupal.t('mm/dd/yy'),
firstDay: 0,
isRTL: 0
}, drupalSettings.jquery.ui.datepicker);
$.datepicker.setDefaults($.datepicker.regional['drupal-locale']);
}
};
})(jQuery, Drupal, drupalSettings);

View file

@ -0,0 +1,108 @@
<?php
/**
* @file
* The API for download and import of translations from remote and local sources.
*/
/**
* Load the common translation API.
*/
// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue: https://www.drupal.org/node/1834298.
require_once __DIR__ . '/locale.translation.inc';
/**
* Builds a batch to check, download and import project translations.
*
* @param array $projects
* Array of project names for which to update the translations. Defaults to
* all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
* @param array $options
* Array of import options. See locale_translate_batch_import_files().
*
* @return array
* Batch definition array.
*/
function locale_translation_batch_update_build($projects = array(), $langcodes = array(), $options = array()) {
module_load_include('compare.inc', 'locale');
$projects = $projects ? $projects : array_keys(locale_translation_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
$status_options = $options;
$status_options['finish_feedback'] = FALSE;
// Check status of local and remote translation files.
$operations = _locale_translation_batch_status_operations($projects, $langcodes, $status_options);
// Download and import translations.
$operations = array_merge($operations, _locale_translation_fetch_operations($projects, $langcodes, $options));
$batch = array(
'operations' => $operations,
'title' => t('Updating translations'),
'progress_message' => '',
'error_message' => t('Error importing translation files'),
'finished' => 'locale_translation_batch_fetch_finished',
'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc',
);
return $batch;
}
/**
* Builds a batch to download and import project translations.
*
* @param array $projects
* Array of project names for which to check the state of translation files.
* Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
* @param array $options
* Array of import options. See locale_translate_batch_import_files().
*
* @return array
* Batch definition array.
*/
function locale_translation_batch_fetch_build($projects = array(), $langcodes = array(), $options = array()) {
$projects = $projects ? $projects : array_keys(locale_translation_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
$batch = array(
'operations' => _locale_translation_fetch_operations($projects, $langcodes, $options),
'title' => t('Updating translations.'),
'progress_message' => '',
'error_message' => t('Error importing translation files'),
'finished' => 'locale_translation_batch_fetch_finished',
'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc',
);
return $batch;
}
/**
* Helper function to construct the batch operations to fetch translations.
*
* @param array $projects
* Array of project names for which to check the state of translation files.
* Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
* @param array $options
* Array of import options.
*
* @return array
* Array of batch operations.
*/
function _locale_translation_fetch_operations($projects, $langcodes, $options) {
$operations = array();
foreach ($projects as $project) {
foreach ($langcodes as $langcode) {
if (locale_translation_use_remote_source()) {
$operations[] = array('locale_translation_batch_fetch_download', array($project, $langcode));
}
$operations[] = array('locale_translation_batch_fetch_import', array($project, $langcode, $options));
}
}
return $operations;
}

View file

@ -0,0 +1,10 @@
name: 'Interface Translation'
type: module
description: 'Translates the built-in user interface.'
configure: locale.translate_page
package: Multilingual
version: VERSION
core: 8.x
dependencies:
- language
- file

View file

@ -0,0 +1,298 @@
<?php
/**
* @file
* Install, update, and uninstall functions for the Locale module.
*/
use Drupal\Core\Url;
use Drupal\Core\Language\Language;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationSelected;
/**
* Implements hook_install().
*/
function locale_install() {
// Create the interface translations directory and ensure it's writable.
if (!$directory = \Drupal::config('locale.settings')->get('translation.path')) {
$site_path = \Drupal::service('site.path');
$directory = $site_path . '/files/translations';
\Drupal::configFactory()->getEditable('locale.settings')->set('translation.path', $directory)->save();
}
file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
}
/**
* Implements hook_uninstall().
*/
function locale_uninstall() {
$config = \Drupal::config('locale.settings');
// Delete all JavaScript translation files.
$locale_js_directory = 'public://' . $config->get('javascript.directory');
if (is_dir($locale_js_directory)) {
$locale_javascripts = \Drupal::state()->get('locale.translation.javascript') ?: array();
foreach ($locale_javascripts as $langcode => $file_suffix) {
if (!empty($file_suffix)) {
file_unmanaged_delete($locale_js_directory . '/' . $langcode . '_' . $file_suffix . '.js');
}
}
// Delete the JavaScript translations directory if empty.
if (!file_scan_directory($locale_js_directory, '/.*/')) {
drupal_rmdir($locale_js_directory);
}
}
// Clear variables.
\Drupal::state()->delete('system.javascript_parsed');
\Drupal::state()->delete('locale.translation.plurals');
\Drupal::state()->delete('locale.translation.javascript');
}
/**
* Implements hook_schema().
*/
function locale_schema() {
$schema['locales_source'] = array(
'description' => 'List of English source strings.',
'fields' => array(
'lid' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'Unique identifier of this string.',
),
'source' => array(
'type' => 'text',
'mysql_type' => 'blob',
'not null' => TRUE,
'description' => 'The original string in English.',
),
'context' => array(
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'The context this string applies to.',
),
'version' => array(
'type' => 'varchar_ascii',
'length' => 20,
'not null' => TRUE,
'default' => 'none',
'description' => 'Version of Drupal where the string was last used (for locales optimization).',
),
),
'primary key' => array('lid'),
'indexes' => array(
'source_context' => array(array('source', 30), 'context'),
),
);
$schema['locales_target'] = array(
'description' => 'Stores translated versions of strings.',
'fields' => array(
'lid' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Source string ID. References {locales_source}.lid.',
),
'translation' => array(
'type' => 'text',
'mysql_type' => 'blob',
'not null' => TRUE,
'description' => 'Translation string value in this language.',
),
'language' => array(
'type' => 'varchar_ascii',
'length' => 12,
'not null' => TRUE,
'default' => '',
'description' => 'Language code. References {language}.langcode.',
),
'customized' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0, // LOCALE_NOT_CUSTOMIZED
'description' => 'Boolean indicating whether the translation is custom to this site.',
),
),
'primary key' => array('language', 'lid'),
'foreign keys' => array(
'locales_source' => array(
'table' => 'locales_source',
'columns' => array('lid' => 'lid'),
),
),
'indexes' => array(
'lid' => array('lid'),
),
);
$schema['locales_location'] = array(
'description' => 'Location information for source strings.',
'fields' => array(
'lid' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'Unique identifier of this location.',
),
'sid' => array(
'type' => 'int',
'not null' => TRUE,
'description' => 'Unique identifier of this string.',
),
'type' => array(
'type' => 'varchar_ascii',
'length' => 50,
'not null' => TRUE,
'default' => '',
'description' => 'The location type (file, config, path, etc).',
),
'name' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'Type dependent location information (file name, path, etc).',
),
'version' => array(
'type' => 'varchar_ascii',
'length' => 20,
'not null' => TRUE,
'default' => 'none',
'description' => 'Version of Drupal where the location was found.',
),
),
'primary key' => array('lid'),
'foreign keys' => array(
'locales_source' => array(
'table' => 'locales_source',
'columns' => array('sid' => 'lid'),
),
),
'indexes' => array(
'string_id' => array('sid'),
'string_type' => array('sid', 'type'),
),
);
$schema['locale_file'] = array(
'description' => 'File import status information for interface translation files.',
'fields' => array(
'project' => array(
'type' => 'varchar_ascii',
'length' => '255',
'not null' => TRUE,
'default' => '',
'description' => 'A unique short name to identify the project the file belongs to.',
),
'langcode' => array(
'type' => 'varchar_ascii',
'length' => '12',
'not null' => TRUE,
'default' => '',
'description' => 'Language code of this translation. References {language}.langcode.',
),
'filename' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'Filename of the imported file.',
),
'version' => array(
'type' => 'varchar',
'length' => '128',
'not null' => TRUE,
'default' => '',
'description' => 'Version tag of the imported file.',
),
'uri' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'URI of the remote file, the resulting local file or the locally imported file.',
),
'timestamp' => array(
'type' => 'int',
'not null' => FALSE,
'default' => 0,
'description' => 'Unix timestamp of the imported file.',
),
'last_checked' => array(
'type' => 'int',
'not null' => FALSE,
'default' => 0,
'description' => 'Unix timestamp of the last time this translation was confirmed to be the most recent release available.',
),
),
'primary key' => array('project', 'langcode'),
);
return $schema;
}
/**
* Implements hook_requirements().
*/
function locale_requirements($phase) {
$requirements = array();
if ($phase == 'runtime') {
$available_updates = array();
$untranslated = array();
$languages = locale_translatable_language_list();
if ($languages) {
// Determine the status of the translation updates per language.
$status = locale_translation_get_status();
if ($status) {
foreach ($status as $project) {
foreach ($project as $langcode => $project_info) {
if (empty($project_info->type)) {
$untranslated[$langcode] = $languages[$langcode]->getName();
}
elseif ($project_info->type == LOCALE_TRANSLATION_LOCAL || $project_info->type == LOCALE_TRANSLATION_REMOTE) {
$available_updates[$langcode] = $languages[$langcode]->getName();
}
}
}
if ($available_updates || $untranslated) {
if ($available_updates) {
$requirements['locale_translation'] = array(
'title' => 'Translation update status',
'value' => \Drupal::l(t('Updates available'), new Url('locale.translate_status')),
'severity' => REQUIREMENT_WARNING,
'description' => t('Updates available for: @languages. See the <a href="@updates">Available translation updates</a> page for more information.', array('@languages' => implode(', ', $available_updates), '@updates' => \Drupal::url('locale.translate_status'))),
);
}
else {
$requirements['locale_translation'] = array(
'title' => 'Translation update status',
'value' => t('Missing translations'),
'severity' => REQUIREMENT_INFO,
'description' => t('Missing translations for: @languages. See the <a href="@updates">Available translation updates</a> page for more information.', array('@languages' => implode(', ', $untranslated), '@updates' => \Drupal::url('locale.translate_status'))),
);
}
}
else {
$requirements['locale_translation'] = array(
'title' => 'Translation update status',
'value' => t('Up to date'),
'severity' => REQUIREMENT_OK,
);
}
}
else {
$requirements['locale_translation'] = array(
'title' => 'Translation update status',
'value' => \Drupal::l(t('Can not determine status'), new Url('locale.translate_status')),
'severity' => REQUIREMENT_WARNING,
'description' => t('No translation status is available. See the <a href="@updates">Available translation updates</a> page for more information.', array('@updates' => \Drupal::url('locale.translate_status'))),
);
}
}
}
return $requirements;
}

View file

@ -0,0 +1,30 @@
drupal.locale.admin:
version: VERSION
js:
locale.admin.js: {}
css:
component:
css/locale.admin.css: {}
dependencies:
- core/jquery
- core/drupal
- core/drupal.form
- core/jquery.once
drupal.locale.datepicker:
version: VERSION
js:
locale.datepicker.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
translations:
# No sensible version can be specified, since the translations may change at
# any time.
js:
# This file does not actually exist; it's a placeholder file that will be
# overriden by locale_js_alter(), to use the file that contains the actual
# translations, for the language used in the current request.
locale.translation.js: {}

View file

@ -0,0 +1,11 @@
locale.translate_page:
title: 'User interface translation'
description: 'Translate the built-in user interface.'
route_name: locale.translate_page
parent: system.admin_config_regional
weight: 15
locale.translate_status:
title: 'Available translation updates'
route_name: locale.translate_status
description: 'Get a status report about available interface translations for your installed modules and themes.'
parent: system.admin_reports

View file

@ -0,0 +1,22 @@
locale.translate_page:
route_name: locale.translate_page
base_route: locale.translate_page
title: Translate
locale.translate_import:
route_name: locale.translate_import
base_route: locale.translate_page
title: Import
weight: 20
locale.translate_export:
route_name: locale.translate_export
base_route: locale.translate_page
title: Export
weight: 30
locale.settings:
route_name: locale.settings
base_route: locale.translate_page
title: Settings
weight: 100

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,124 @@
<?php
/**
* @file
* Interface translation summary, editing and deletion user interfaces.
*/
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Url;
use Drupal\Core\Render\Element;
use Drupal\locale\SourceString;
use Drupal\locale\TranslationString;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Page callback: Checks for translation updates and displays the status.
*
* Manually checks the translation status without the use of cron.
*
* @see locale_menu()
*/
function locale_translation_manual_status() {
module_load_include('compare.inc', 'locale');
// Check the translation status of all translatable projects in all languages.
// First we clear the cached list of projects. Although not strictly
// necessary, this is helpful in case the project list is out of sync.
locale_translation_flush_projects();
locale_translation_check_projects();
// Execute a batch if required. A batch is only used when remote files
// are checked.
if (batch_get()) {
return batch_process('admin/reports/translations');
}
return new RedirectResponse(\Drupal::url('locale.translate_status', array(), array('absolute' => TRUE)));
}
/**
* Prepares variables for translation status information templates.
*
* Translation status information is displayed per language.
*
* Default template: locale-translate-edit-form-strings.html.twig.
*
* @param array $variables
* An associative array containing:
* - updates: The projects which have updates.
* - not_found: The projects which updates are not found.
*
* @see \Drupal\locale\Form\TranslationStatusForm
*/
function template_preprocess_locale_translation_update_info(array &$variables) {
$details = array();
// Build output for available updates.
if (isset($variables['updates'])) {
$releases = array();
if ($variables['updates']) {
foreach ($variables['updates'] as $update) {
$modules[] = $update['name'];
$releases[] = SafeMarkup::format('@module (@date)', array(
'@module' => $update['name'],
'@date' => format_date($update['timestamp'], 'html_date'),
));
}
$variables['modules'] = $modules;
}
$details['available_updates_list'] = array(
'#theme' => 'item_list',
'#items' => $releases,
);
}
// Build output for updates not found.
if (isset($variables['not_found'])) {
$releases = array();
$variables['missing_updates_status'] = \Drupal::translation()->formatPlural(count($variables['not_found']), 'Missing translations for one project', 'Missing translations for @count projects');
if ($variables['not_found']) {
foreach ($variables['not_found'] as $update) {
$version = $update['version'] ? $update['version'] : t('no version');
$releases[] = SafeMarkup::format('@module (@version). !info', array(
'@module' => $update['name'],
'@version' => $version,
'!info' => $update['info'],
));
}
}
$details['missing_updates_list'] = array(
'#theme' => 'item_list',
'#items' => $releases,
);
// Prefix the missing updates list if there is an available updates lists
// before it.
if (!empty($details['available_updates_list']['#items'])) {
$details['missing_updates_list']['#prefix'] = t('Missing translations for:');
}
}
$variables['details'] = $details;
}
/**
* Prepares variables for most recent translation update templates.
*
* Displays the last time we checked for locale update data. In addition to
* properly formatting the given timestamp, this function also provides a "Check
* manually" link that refreshes the available update and redirects back to the
* same page.
*
* Default template: locale-translation-last-check.html.twig.
*
* @param array $variables
* An associative array containing:
* - last: The timestamp when the site last checked for available updates.
*
* @see \Drupal\locale\Form\TranslationStatusForm
*/
function template_preprocess_locale_translation_last_check(array &$variables) {
$last = $variables['last'];
$variables['last_checked'] = ($last != NULL);
$variables['time'] = \Drupal::service('date.formatter')->formatTimeDiffSince($last);
$variables['link'] = \Drupal::l(t('Check manually'), new Url('locale.check_translation', array(), array('query' => \Drupal::destination()->getAsArray())));
}

View file

@ -0,0 +1,4 @@
translate interface:
title: 'Translate interface text'
description: 'Translate any interface text including configuration shipped with modules and themes.'
restrict access: true

View file

@ -0,0 +1,46 @@
locale.settings:
path: '/admin/config/regional/translate/settings'
defaults:
_form: 'Drupal\locale\Form\LocaleSettingsForm'
_title: 'Settings'
requirements:
_permission: 'translate interface'
locale.check_translation:
path: '/admin/reports/translations/check'
defaults:
_controller: 'Drupal\locale\Controller\LocaleController::checkTranslation'
requirements:
_permission: 'translate interface'
locale.translate_page:
path: '/admin/config/regional/translate'
defaults:
_controller: '\Drupal\locale\Controller\LocaleController::translatePage'
_title: 'User interface translation'
requirements:
_permission: 'translate interface'
locale.translate_import:
path: '/admin/config/regional/translate/import'
defaults:
_form: '\Drupal\locale\Form\ImportForm'
_title: 'Import'
requirements:
_permission: 'translate interface'
locale.translate_export:
path: '/admin/config/regional/translate/export'
defaults:
_form: '\Drupal\locale\Form\ExportForm'
_title: 'Export'
requirements:
_permission: 'translate interface'
locale.translate_status:
path: '/admin/reports/translations'
defaults:
_form: '\Drupal\locale\Form\TranslationStatusForm'
_title: 'Available translation updates'
requirements:
_permission: 'translate interface'

View file

@ -0,0 +1,36 @@
services:
locale.default.config.storage:
class: Drupal\locale\LocaleDefaultConfigStorage
arguments: ['@config.storage', '@language_manager']
public: false
locale.config_manager:
class: Drupal\locale\LocaleConfigManager
arguments: ['@config.storage', '@locale.storage', '@config.factory', '@config.typed', '@language_manager', '@locale.default.config.storage']
locale.storage:
class: Drupal\locale\StringDatabaseStorage
arguments: ['@database']
tags:
- { name: backend_overridable }
locale.project:
class: Drupal\locale\LocaleProjectStorage
arguments: ['@keyvalue']
string_translator.locale.lookup:
class: Drupal\locale\LocaleTranslation
arguments: ['@locale.storage', '@cache.default', '@lock', '@config.factory', '@language_manager', '@request_stack']
tags:
- { name: string_translator }
- { name: needs_destruction }
stream_wrapper.translations:
class: Drupal\locale\StreamWrapper\TranslationsStream
tags:
- { name: stream_wrapper, scheme: translations }
locale.config_subscriber:
class: Drupal\locale\LocaleConfigSubscriber
arguments: ['@config.factory', '@locale.config_manager']
tags:
- { name: event_subscriber }
locale.locale_translation_cache_tag:
class: Drupal\locale\EventSubscriber\LocaleTranslationCacheTag
arguments: ['@cache_tags.invalidator']
tags:
- { name: event_subscriber }

View file

@ -0,0 +1,441 @@
<?php
/**
* @file
* Common API for interface translation.
*/
/**
* Comparison result of source files timestamps.
*
* Timestamp of source 1 is less than the timestamp of source 2.
*
* @see _locale_translation_source_compare()
*/
const LOCALE_TRANSLATION_SOURCE_COMPARE_LT = -1;
/**
* Comparison result of source files timestamps.
*
* Timestamp of source 1 is equal to the timestamp of source 2.
*
* @see _locale_translation_source_compare()
*/
const LOCALE_TRANSLATION_SOURCE_COMPARE_EQ = 0;
/**
* Comparison result of source files timestamps.
*
* Timestamp of source 1 is greater than the timestamp of source 2.
*
* @see _locale_translation_source_compare()
*/
const LOCALE_TRANSLATION_SOURCE_COMPARE_GT = 1;
/**
* Get array of projects which are available for interface translation.
*
* This project data contains all projects which will be checked for available
* interface translations.
*
* For full functionality this function depends on Update module.
* When Update module is enabled the project data will contain the most recent
* module status; both in enabled status as in version. When Update module is
* disabled this function will return the last known module state. The status
* will only be updated once Update module is enabled.
*
* @param array $project_names
* Array of names of the projects to get.
*
* @return array
* Array of project data for translation update.
*
* @see locale_translation_build_projects()
*/
function locale_translation_get_projects(array $project_names = array()) {
$projects = &drupal_static(__FUNCTION__, array());
if (empty($projects)) {
// Get project data from the database.
$row_count = \Drupal::service('locale.project')->countProjects();
// https://www.drupal.org/node/1777106 is a follow-up issue to make the
// check for possible out-of-date project information more robust.
if ($row_count == 0 && \Drupal::moduleHandler()->moduleExists('update')) {
module_load_include('compare.inc', 'locale');
// At least the core project should be in the database, so we build the
// data if none are found.
locale_translation_build_projects();
}
$projects = \Drupal::service('locale.project')->getAll();
array_walk($projects, function(&$project) {
$project = (object) $project;
});
}
// Return the requested project names or all projects.
if ($project_names) {
return array_intersect_key($projects, array_combine($project_names, $project_names));
}
return $projects;
}
/**
* Clears the projects cache.
*/
function locale_translation_clear_cache_projects() {
drupal_static_reset('locale_translation_get_projects');
}
/**
* Loads cached translation sources containing current translation status.
*
* @param array $projects
* Array of project names. Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
*
* @return array
* Array of source objects. Keyed with <project name>:<language code>.
*
* @see locale_translation_source_build()
*/
function locale_translation_load_sources(array $projects = NULL, array $langcodes = NULL) {
$sources = array();
$projects = $projects ? $projects : array_keys(locale_translation_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
// Load source data from locale_translation_status cache.
$status = locale_translation_get_status();
// Use only the selected projects and languages for update.
foreach ($projects as $project) {
foreach ($langcodes as $langcode) {
$sources[$project][$langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL;
}
}
return $sources;
}
/**
* Build translation sources.
*
* @param array $projects
* Array of project names. Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
*
* @return array
* Array of source objects. Keyed by project name and language code.
*
* @see locale_translation_source_build()
*/
function locale_translation_build_sources(array $projects = array(), array $langcodes = array()) {
$sources = array();
$projects = locale_translation_get_projects($projects);
$langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
foreach ($projects as $project) {
foreach ($langcodes as $langcode) {
$source = locale_translation_source_build($project, $langcode);
$sources[$source->name][$source->langcode] = $source;
}
}
return $sources;
}
/**
* Checks whether a po file exists in the local filesystem.
*
* It will search in the directory set in the translation source. Which defaults
* to the "translations://" stream wrapper path. The directory may contain any
* valid stream wrapper.
*
* The "local" files property of the source object contains the definition of a
* po file we are looking for. The file name defaults to
* %project-%version.%language.po. Per project this value can be overridden
* using the server_pattern directive in the module's .info.yml file or by using
* hook_locale_translation_projects_alter().
*
* @param object $source
* Translation source object.
*
* @return object
* Source file object of the po file, updated with:
* - "uri": File name and path.
* - "timestamp": Last updated time of the po file.
* FALSE if the file is not found.
*
* @see locale_translation_source_build()
*/
function locale_translation_source_check_file($source) {
if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
$source_file = $source->files[LOCALE_TRANSLATION_LOCAL];
$directory = $source_file->directory;
$filename = '/' . preg_quote($source_file->filename) . '$/';
if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) {
$file = current($files);
$source_file->uri = $file->uri;
$source_file->timestamp = filemtime($file->uri);
return $source_file;
}
}
return FALSE;
}
/**
* Builds abstract translation source.
*
* @param object $project
* Project object.
* @param string $langcode
* Language code.
* @param string $filename
* (optional) File name of translation file. May contain placeholders.
* Defaults to the default translation filename from the settings.
*
* @return object
* Source object:
* - "project": Project name.
* - "name": Project name (inherited from project).
* - "language": Language code.
* - "core": Core version (inherited from project).
* - "version": Project version (inherited from project).
* - "project_type": Project type (inherited from project).
* - "files": Array of file objects containing properties of local and remote
* translation files.
* Other processes can add the following properties:
* - "type": Most recent translation source found. LOCALE_TRANSLATION_REMOTE
* and LOCALE_TRANSLATION_LOCAL indicate available new translations,
* LOCALE_TRANSLATION_CURRENT indicate that the current translation is them
* most recent. "type" corresponds with a key of the "files" array.
* - "timestamp": The creation time of the "type" translation (file).
* - "last_checked": The time when the "type" translation was last checked.
* The "files" array can hold file objects of type:
* LOCALE_TRANSLATION_LOCAL, LOCALE_TRANSLATION_REMOTE and
* LOCALE_TRANSLATION_CURRENT. Each contains following properties:
* - "type": The object type (LOCALE_TRANSLATION_LOCAL,
* LOCALE_TRANSLATION_REMOTE, etc. see above).
* - "project": Project name.
* - "langcode": Language code.
* - "version": Project version.
* - "uri": Local or remote file path.
* - "directory": Directory of the local po file.
* - "filename": File name.
* - "timestamp": Timestamp of the file.
* - "keep": TRUE to keep the downloaded file.
*/
function locale_translation_source_build($project, $langcode, $filename = NULL) {
// Follow-up issue: https://www.drupal.org/node/1842380.
// Convert $source object to a TranslatableProject class and use a typed class
// for $source-file.
// Create a source object with data of the project object.
$source = clone $project;
$source->project = $project->name;
$source->langcode = $langcode;
$source->type = '';
$source->timestamp = 0;
$source->last_checked = 0;
$filename = $filename ? $filename : \Drupal::config('locale.settings')->get('translation.default_filename');
// If the server_pattern contains a remote file path we will check for a
// remote file. The local version of this file will only be checked if a
// translations directory has been defined. If the server_pattern is a local
// file path we will only check for a file in the local file system.
$files = array();
if (_locale_translation_file_is_remote($source->server_pattern)) {
$files[LOCALE_TRANSLATION_REMOTE] = (object) array(
'project' => $project->name,
'langcode' => $langcode,
'version' => $project->version,
'type' => LOCALE_TRANSLATION_REMOTE,
'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
'uri' => locale_translation_build_server_pattern($source, $source->server_pattern),
);
$files[LOCALE_TRANSLATION_LOCAL] = (object) array(
'project' => $project->name,
'langcode' => $langcode,
'version' => $project->version,
'type' => LOCALE_TRANSLATION_LOCAL,
'filename' => locale_translation_build_server_pattern($source, $filename),
'directory' => 'translations://',
);
$files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . $files[LOCALE_TRANSLATION_LOCAL]->filename;
}
else {
$files[LOCALE_TRANSLATION_LOCAL] = (object) array(
'project' => $project->name,
'langcode' => $langcode,
'version' => $project->version,
'type' => LOCALE_TRANSLATION_LOCAL,
'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
'directory' => locale_translation_build_server_pattern($source, drupal_dirname($source->server_pattern)),
);
$files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . '/' . $files[LOCALE_TRANSLATION_LOCAL]->filename;
}
$source->files = $files;
// If this project+language is already translated, we add its status and
// update the current translation timestamp and last_updated time. If the
// project+language is not translated before, create a new record.
$history = locale_translation_get_file_history();
if (isset($history[$project->name][$langcode]) && $history[$project->name][$langcode]->timestamp) {
$source->files[LOCALE_TRANSLATION_CURRENT] = $history[$project->name][$langcode];
$source->type = LOCALE_TRANSLATION_CURRENT;
$source->timestamp = $history[$project->name][$langcode]->timestamp;
$source->last_checked = $history[$project->name][$langcode]->last_checked;
}
else {
locale_translation_update_file_history($source);
}
return $source;
}
/**
* Build path to translation source, out of a server path replacement pattern.
*
* @param object $project
* Project object containing data to be inserted in the template.
* @param string $template
* String containing placeholders. Available placeholders:
* - "%project": Project name.
* - "%version": Project version.
* - "%core": Project core version.
* - "%language": Language code.
*
* @return string
* String with replaced placeholders.
*/
function locale_translation_build_server_pattern($project, $template) {
$variables = array(
'%project' => $project->name,
'%version' => $project->version,
'%core' => $project->core,
'%language' => isset($project->langcode) ? $project->langcode : '%language',
);
return strtr($template, $variables);
}
/**
* Populate a queue with project to check for translation updates.
*/
function locale_cron_fill_queue() {
$updates = array();
$config = \Drupal::config('locale.settings');
// Determine which project+language should be updated.
$last = REQUEST_TIME - $config->get('translation.update_interval_days') * 3600 * 24;
$projects = \Drupal::service('locale.project')->getAll();
$projects = array_filter($projects, function($project) {
return $project['status'] == 1;
});
$files = db_select('locale_file', 'f')
->condition('f.project', array_keys($projects), 'IN')
->condition('f.last_checked', $last, '<')
->fields('f', array('project', 'langcode'))
->execute()->fetchAll();
foreach ($files as $file) {
$updates[$file->project][] = $file->langcode;
// Update the last_checked timestamp of the project+language that will
// be checked for updates.
db_update('locale_file')
->fields(array('last_checked' => REQUEST_TIME))
->condition('project', $file->project)
->condition('langcode', $file->langcode)
->execute();
}
// For each project+language combination a number of tasks are added to
// the queue.
if ($updates) {
module_load_include('fetch.inc', 'locale');
$options = _locale_translation_default_update_options();
$queue = \Drupal::queue('locale_translation', TRUE);
foreach ($updates as $project => $languages) {
$batch = locale_translation_batch_update_build(array($project), $languages, $options);
foreach ($batch['operations'] as $item) {
$queue->createItem($item);
}
}
}
}
/**
* Determine if a file is a remote file.
*
* @param string $uri
* The URI or URI pattern of the file.
*
* @return bool
* TRUE if the $uri is a remote file.
*/
function _locale_translation_file_is_remote($uri) {
$scheme = file_uri_scheme($uri);
if ($scheme) {
return !drupal_realpath($scheme . '://');
}
return FALSE;
}
/**
* Compare two update sources, looking for the newer one.
*
* The timestamp property of the source objects are used to determine which is
* the newer one.
*
* @param object $source1
* Source object of the first translation source.
* @param object $source2
* Source object of available update.
*
* @return int
* - "LOCALE_TRANSLATION_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1
* is missing.
* - "LOCALE_TRANSLATION_SOURCE_COMPARE_EQ": $source1 == $source2 OR both
* $source1 and $source2 are missing.
* - "LOCALE_TRANSLATION_SOURCE_COMPARE_GT": $source1 > $source2 OR $source2
* is missing.
*/
function _locale_translation_source_compare($source1, $source2) {
if (isset($source1->timestamp) && isset($source2->timestamp)) {
if ($source1->timestamp == $source2->timestamp) {
return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ;
}
else {
return $source1->timestamp > $source2->timestamp ? LOCALE_TRANSLATION_SOURCE_COMPARE_GT : LOCALE_TRANSLATION_SOURCE_COMPARE_LT;
}
}
elseif (isset($source1->timestamp) && !isset($source2->timestamp)) {
return LOCALE_TRANSLATION_SOURCE_COMPARE_GT;
}
elseif (!isset($source1->timestamp) && isset($source2->timestamp)) {
return LOCALE_TRANSLATION_SOURCE_COMPARE_LT;
}
else {
return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ;
}
}
/**
* Returns default import options for translation update.
*
* @return array
* Array of translation import options.
*/
function _locale_translation_default_update_options() {
$config = \Drupal::config('locale.settings');
return array(
'customized' => LOCALE_NOT_CUSTOMIZED,
'overwrite_options' => array(
'not_customized' => $config->get('translation.overwrite_not_customized'),
'customized' => $config->get('translation.overwrite_customized'),
),
'finish_feedback' => TRUE,
'use_remote' => locale_translation_use_remote_source(),
);
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\locale\Controller\LocaleController.
*/
namespace Drupal\locale\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Return response for manual check translations.
*/
class LocaleController extends ControllerBase {
/**
* Checks for translation updates and displays the translations status.
*
* Manually checks the translation status without the use of cron.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirection to translations reports page.
*/
public function checkTranslation() {
$this->moduleHandler()->loadInclude('locale', 'inc', 'locale.compare');
// Check translation status of all translatable project in all languages.
// First we clear the cached list of projects. Although not strictly
// necessary, this is helpful in case the project list is out of sync.
locale_translation_flush_projects();
locale_translation_check_projects();
// Execute a batch if required. A batch is only used when remote files
// are checked.
if (batch_get()) {
return batch_process('admin/reports/translations');
}
return $this->redirect('locale.translate_status');
}
/**
* Shows the string search screen.
*
* @return array
* The render array for the string search screen.
*/
public function translatePage() {
return array(
'filter' => $this->formBuilder()->getForm('Drupal\locale\Form\TranslateFilterForm'),
'form' => $this->formBuilder()->getForm('Drupal\locale\Form\TranslateEditForm'),
);
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\locale\EventSubscriber\LocaleTranslationCacheTag.
*/
namespace Drupal\locale\EventSubscriber;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\locale\LocaleEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A subscriber invalidating cache tags when translating a string.
*/
class LocaleTranslationCacheTag implements EventSubscriberInterface {
/**
* The cache tags invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagsInvalidator;
/**
* Constructs a LocaleTranslationCacheTag object.
*
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* The cache tags invalidator.
*/
public function __construct(CacheTagsInvalidatorInterface $cache_tags_invalidator) {
$this->cacheTagsInvalidator = $cache_tags_invalidator;
}
/**
* Invalidate cache tags whenever a string is translated.
*/
public function saveTranslation() {
$this->cacheTagsInvalidator->invalidateTags(['rendered', 'locale']);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[LocaleEvents::SAVE_TRANSLATION][] = ['saveTranslation'];
return $events;
}
}

View file

@ -0,0 +1,179 @@
<?php
/**
* @file
* Contains \Drupal\locale\Form\ExportForm.
*/
namespace Drupal\locale\Form;
use Drupal\Component\Gettext\PoStreamWriter;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\locale\PoDatabaseReader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* Form for the Gettext translation files export form.
*/
class ExportForm extends FormBase {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new ExportForm.
*
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(LanguageManagerInterface $language_manager) {
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'locale_translate_export_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$languages = $this->languageManager->getLanguages();
$language_options = array();
foreach ($languages as $langcode => $language) {
if (locale_is_translatable($langcode)) {
$language_options[$langcode] = $language->getName();
}
}
$language_default = $this->languageManager->getDefaultLanguage();
if (empty($language_options)) {
$form['langcode'] = array(
'#type' => 'value',
'#value' => LanguageInterface::LANGCODE_SYSTEM,
);
$form['langcode_text'] = array(
'#type' => 'item',
'#title' => $this->t('Language'),
'#markup' => $this->t('No language available. The export will only contain source strings.'),
);
}
else {
$form['langcode'] = array(
'#type' => 'select',
'#title' => $this->t('Language'),
'#options' => $language_options,
'#default_value' => $language_default->getId(),
'#empty_option' => $this->t('Source text only, no translations'),
'#empty_value' => LanguageInterface::LANGCODE_SYSTEM,
);
$form['content_options'] = array(
'#type' => 'details',
'#title' => $this->t('Export options'),
'#collapsed' => TRUE,
'#tree' => TRUE,
'#states' => array(
'invisible' => array(
':input[name="langcode"]' => array('value' => LanguageInterface::LANGCODE_SYSTEM),
),
),
);
$form['content_options']['not_customized'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Include non-customized translations'),
'#default_value' => TRUE,
);
$form['content_options']['customized'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Include customized translations'),
'#default_value' => TRUE,
);
$form['content_options']['not_translated'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Include untranslated text'),
'#default_value' => TRUE,
);
}
$form['actions'] = array(
'#type' => 'actions',
);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Export'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// If template is required, language code is not given.
if ($form_state->getValue('langcode') != LanguageInterface::LANGCODE_SYSTEM) {
$language = $this->languageManager->getLanguage($form_state->getValue('langcode'));
}
else {
$language = NULL;
}
$content_options = $form_state->getValue('content_options', array());
$reader = new PoDatabaseReader();
$language_name = '';
if ($language != NULL) {
$reader->setLangcode($language->getId());
$reader->setOptions($content_options);
$languages = $this->languageManager->getLanguages();
$language_name = isset($languages[$language->getId()]) ? $languages[$language->getId()]->getName() : '';
$filename = $language->getId() .'.po';
}
else {
// Template required.
$filename = 'drupal.pot';
}
$item = $reader->readItem();
if (!empty($item)) {
$uri = tempnam('temporary://', 'po_');
$header = $reader->getHeader();
$header->setProjectName($this->config('system.site')->get('name'));
$header->setLanguageName($language_name);
$writer = new PoStreamWriter();
$writer->setUri($uri);
$writer->setHeader($header);
$writer->open();
$writer->writeItem($item);
$writer->writeItems($reader);
$writer->close();
$response = new BinaryFileResponse($uri);
$response->setContentDisposition('attachment', $filename);
$form_state->setResponse($response);
}
else {
drupal_set_message($this->t('Nothing to export.'));
}
}
}

View file

@ -0,0 +1,200 @@
<?php
/**
* @file
* Contains \Drupal\locale\Form\ImportForm.
*/
namespace Drupal\locale\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\language\ConfigurableLanguageManagerInterface;
use Drupal\language\Entity\ConfigurableLanguage;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form constructor for the translation import screen.
*/
class ImportForm extends FormBase {
/**
* Uploaded file entity.
*
* @var \Drupal\file\Entity\File
*/
protected $file;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The configurable language manager.
*
* @var \Drupal\language\ConfigurableLanguageManagerInterface
*/
protected $languageManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('language_manager')
);
}
/**
* Constructs a form for language import.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager
* The configurable language manager.
*/
public function __construct(ModuleHandlerInterface $module_handler, ConfigurableLanguageManagerInterface $language_manager) {
$this->moduleHandler = $module_handler;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'locale_translate_import_form';
}
/**
* Form constructor for the translation import screen.
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$languages = $this->languageManager->getLanguages();
// Initialize a language list to the ones available, including English if we
// are to translate Drupal to English as well.
$existing_languages = array();
foreach ($languages as $langcode => $language) {
if (locale_is_translatable($langcode)) {
$existing_languages[$langcode] = $language->getName();
}
}
// If we have no languages available, present the list of predefined
// languages only. If we do have already added languages, set up two option
// groups with the list of existing and then predefined languages.
if (empty($existing_languages)) {
$language_options = $this->languageManager->getStandardLanguageListWithoutConfigured();
$default = key($language_options);
}
else {
$default = key($existing_languages);
$language_options = array(
$this->t('Existing languages') => $existing_languages,
$this->t('Languages not yet added') => $this->languageManager->getStandardLanguageListWithoutConfigured(),
);
}
$validators = array(
'file_validate_extensions' => array('po'),
'file_validate_size' => array(file_upload_max_size()),
);
$form['file'] = array(
'#type' => 'file',
'#title' => $this->t('Translation file'),
'#description' => array(
'#theme' => 'file_upload_help',
'#description' => $this->t('A Gettext Portable Object file.'),
'#upload_validators' => $validators,
),
'#size' => 50,
'#upload_validators' => $validators,
'#attributes' => array('class' => array('file-import-input')),
);
$form['langcode'] = array(
'#type' => 'select',
'#title' => $this->t('Language'),
'#options' => $language_options,
'#default_value' => $default,
'#attributes' => array('class' => array('langcode-input')),
);
$form['customized'] = array(
'#title' => $this->t('Treat imported strings as custom translations'),
'#type' => 'checkbox',
);
$form['overwrite_options'] = array(
'#type' => 'container',
'#tree' => TRUE,
);
$form['overwrite_options']['not_customized'] = array(
'#title' => $this->t('Overwrite non-customized translations'),
'#type' => 'checkbox',
'#states' => array(
'checked' => array(
':input[name="customized"]' => array('checked' => TRUE),
),
),
);
$form['overwrite_options']['customized'] = array(
'#title' => $this->t('Overwrite existing customized translations'),
'#type' => 'checkbox',
);
$form['actions'] = array(
'#type' => 'actions',
);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Import'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$this->file = file_save_upload('file', $form['file']['#upload_validators'], 'translations://', 0);
// Ensure we have the file uploaded.
if (!$this->file) {
$form_state->setErrorByName('file', $this->t('File to import not found.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
\Drupal::moduleHandler()->loadInclude('locale', 'translation.inc');
// Add language, if not yet supported.
$language = $this->languageManager->getLanguage($form_state->getValue('langcode'));
if (empty($language)) {
$language = ConfigurableLanguage::createFromLangcode($form_state->getValue('langcode'));
$language->save();
drupal_set_message($this->t('The language %language has been created.', array('%language' => $this->t($language->label()))));
}
$options = array_merge(_locale_translation_default_update_options(), array(
'langcode' => $form_state->getValue('langcode'),
'overwrite_options' => $form_state->getValue('overwrite_options'),
'customized' => $form_state->getValue('customized') ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED,
));
$this->moduleHandler->loadInclude('locale', 'bulk.inc');
$file = locale_translate_file_attach_properties($this->file, $options);
$batch = locale_translate_batch_build(array($file->uri => $file), $options);
batch_set($batch);
// Create or update all configuration translations for this language.
\Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
if ($batch = locale_config_batch_update_components($options, array($form_state->getValue('langcode')))) {
batch_set($batch);
}
$form_state->setRedirect('locale.translate_page');
}
}

View file

@ -0,0 +1,144 @@
<?php
/**
* @file
* Contains \Drupal\locale\Form\LocaleSettingsForm.
*/
namespace Drupal\locale\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure locale settings for this site.
*/
class LocaleSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'locale_translate_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['locale.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('locale.settings');
$form['update_interval_days'] = array(
'#type' => 'radios',
'#title' => $this->t('Check for updates'),
'#default_value' => $config->get('translation.update_interval_days'),
'#options' => array(
'0' => $this->t('Never (manually)'),
'7' => $this->t('Weekly'),
'30' => $this->t('Monthly'),
),
'#description' => $this->t('Select how frequently you want to check for new interface translations for your currently installed modules and themes. <a href="@url">Check updates now</a>.', array('@url' => $this->url('locale.check_translation'))),
);
if ($directory = $config->get('translation.path')) {
$description = $this->t('Translation files are stored locally in the %path directory. You can change this directory on the <a href="@url">File system</a> configuration page.', array('%path' => $directory, '@url' => $this->url('system.file_system_settings')));
}
else {
$description = $this->t('Translation files will not be stored locally. Change the Interface translation directory on the <a href="@url">File system configuration</a> page.', array('@url' => $this->url('system.file_system_settings')));
}
$form['#translation_directory'] = $directory;
$form['use_source'] = array(
'#type' => 'radios',
'#title' => $this->t('Translation source'),
'#default_value' => $config->get('translation.use_source'),
'#options' => array(
LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL => $this->t('Drupal translation server and local files'),
LOCALE_TRANSLATION_USE_SOURCE_LOCAL => $this->t('Local files only'),
),
'#description' => $this->t('The source of translation files for automatic interface translation.') . ' ' . $description,
);
if ($config->get('translation.overwrite_not_customized') == FALSE) {
$default = LOCALE_TRANSLATION_OVERWRITE_NONE;
}
elseif ($config->get('translation.overwrite_customized') == TRUE) {
$default = LOCALE_TRANSLATION_OVERWRITE_ALL;
}
else {
$default = LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED;
}
$form['overwrite'] = array(
'#type' => 'radios',
'#title' => $this->t('Import behavior'),
'#default_value' => $default,
'#options' => array(
LOCALE_TRANSLATION_OVERWRITE_NONE => $this->t("Don't overwrite existing translations."),
LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED => $this->t('Only overwrite imported translations, customized translations are kept.'),
LOCALE_TRANSLATION_OVERWRITE_ALL => $this->t('Overwrite existing translations.'),
),
'#description' => $this->t('How to treat existing translations when automatically updating the interface translations.'),
);
return parent::buildForm($form, $form_state);
}
/**
* Implements \Drupal\Core\Form\FormInterface::validateForm().
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
if (empty($form['#translation_directory']) && $form_state->getValue('use_source') == LOCALE_TRANSLATION_USE_SOURCE_LOCAL) {
$form_state->setErrorByName('use_source', $this->t('You have selected local translation source, but no <a href="@url">Interface translation directory</a> was configured.', array('@url' => $this->url('system.file_system_settings'))));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$config = $this->config('locale.settings');
$config->set('translation.update_interval_days', $values['update_interval_days'])->save();
$config->set('translation.use_source', $values['use_source'])->save();
switch ($values['overwrite']) {
case LOCALE_TRANSLATION_OVERWRITE_ALL:
$config
->set('translation.overwrite_customized', TRUE)
->set('translation.overwrite_not_customized', TRUE)
->save();
break;
case LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED:
$config
->set('translation.overwrite_customized', FALSE)
->set('translation.overwrite_not_customized', TRUE)
->save();
break;
case LOCALE_TRANSLATION_OVERWRITE_NONE:
$config
->set('translation.overwrite_customized', FALSE)
->set('translation.overwrite_not_customized', FALSE)
->save();
break;
}
// Invalidate the cached translation status when the configuration setting
// of 'use_source' changes.
if ($form['use_source']['#default_value'] != $form_state->getValue('use_source')) {
locale_translation_clear_status();
}
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,239 @@
<?php
/**
* @file
* Contains \Drupal\locale\Form\TranslateEditForm.
*/
namespace Drupal\locale\Form;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\locale\SourceString;
/**
* Defines a translation edit form.
*/
class TranslateEditForm extends TranslateFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'locale_translate_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$filter_values = $this->translateFilterValues();
$langcode = $filter_values['langcode'];
$this->languageManager->reset();
$languages = $this->languageManager->getLanguages();
$langname = isset($langcode) ? $languages[$langcode]->getName() : "- None -";
$form['#attached']['library'][] = 'locale/drupal.locale.admin';
$form['langcode'] = array(
'#type' => 'value',
'#value' => $filter_values['langcode'],
);
$form['strings'] = array(
'#type' => 'table',
'#tree' => TRUE,
'#language' => $langname,
'#header' => [
$this->t('Source string'),
$this->t('Translation for @language', ['@language' => $langname]),
],
'#empty' => $this->t('No strings available.'),
'#attributes' => ['class' => ['locale-translate-edit-table']],
);
if (isset($langcode)) {
$strings = $this->translateFilterLoadStrings();
$plurals = $this->getNumberOfPlurals($langcode);
foreach ($strings as $string) {
// Cast into source string, will do for our purposes.
$source = new SourceString($string);
// Split source to work with plural values.
$source_array = $source->getPlurals();
$translation_array = $string->getPlurals();
if (count($source_array) == 1) {
// Add original string value and mark as non-plural.
$plural = FALSE;
$form['strings'][$string->lid]['original'] = array(
'#type' => 'item',
'#title' => $this->t('Source string (@language)', array('@language' => $this->t('Built-in English'))),
'#title_display' => 'invisible',
'#markup' => '<span lang="en">' . SafeMarkup::checkPlain($source_array[0]) . '</span>',
);
}
else {
// Add original string value and mark as plural.
$plural = TRUE;
$original_singular = [
'#type' => 'item',
'#title' => $this->t('Singular form'),
'#markup' => '<span lang="en">' . SafeMarkup::checkPlain($source_array[0]) . '</span>',
'#prefix' => '<span class="visually-hidden">' . $this->t('Source string (@language)', array('@language' => $this->t('Built-in English'))) . '</span>',
];
$original_plural = [
'#type' => 'item',
'#title' => $this->t('Plural form'),
'#markup' => '<span lang="en">' . SafeMarkup::checkPlain($source_array[1]) . '</span>',
];
$form['strings'][$string->lid]['original'] = [
$original_singular,
['#markup' => '<br>'],
$original_plural,
];
}
if (!empty($string->context)) {
$form['strings'][$string->lid]['original'][] = [
'#type' => 'inline_template',
'#template' => '<br><small>{{ context_title }}: <span lang="en">{{ context }}</span></small>',
'#context' => [
'context_title' => $this->t('In Context'),
'context' => $string->context,
],
];
}
// Approximate the number of rows to use in the default textarea.
$rows = min(ceil(str_word_count($source_array[0]) / 12), 10);
if (!$plural) {
$form['strings'][$string->lid]['translations'][0] = array(
'#type' => 'textarea',
'#title' => $this->t('Translated string (@language)', array('@language' => $langname)),
'#title_display' => 'invisible',
'#rows' => $rows,
'#default_value' => $translation_array[0],
'#attributes' => array('lang' => $langcode),
);
}
else {
// Add a textarea for each plural variant.
for ($i = 0; $i < $plurals; $i++) {
$form['strings'][$string->lid]['translations'][$i] = array(
'#type' => 'textarea',
'#title' => ($i == 0 ? $this->t('Singular form') : $this->formatPlural($i, 'First plural form', '@count. plural form')),
'#rows' => $rows,
'#default_value' => isset($translation_array[$i]) ? $translation_array[$i] : '',
'#attributes' => array('lang' => $langcode),
'#prefix' => $i == 0 ? ('<span class="visually-hidden">' . $this->t('Translated string (@language)', array('@language' => $langname)) . '</span>') : '',
);
}
if ($plurals == 2) {
// Simplify interface text for the most common case.
$form['strings'][$string->lid]['translations'][1]['#title'] = $this->t('Plural form');
}
}
}
if (count(Element::children($form['strings']))) {
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Save translations'),
);
}
}
$form['pager']['#type'] = 'pager';
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$langcode = $form_state->getValue('langcode');
foreach ($form_state->getValue('strings') as $lid => $translations) {
foreach ($translations['translations'] as $key => $value) {
if (!locale_string_is_safe($value)) {
$form_state->setErrorByName("strings][$lid][translations][$key", $this->t('The submitted string contains disallowed HTML: %string', array('%string' => $value)));
$form_state->setErrorByName("translations][$langcode][$key", $this->t('The submitted string contains disallowed HTML: %string', array('%string' => $value)));
$this->logger('locale')->warning('Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value));
}
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$langcode = $form_state->getValue('langcode');
$updated = array();
// Preload all translations for strings in the form.
$lids = array_keys($form_state->getValue('strings'));
$existing_translation_objects = array();
foreach ($this->localeStorage->getTranslations(array('lid' => $lids, 'language' => $langcode, 'translated' => TRUE)) as $existing_translation_object) {
$existing_translation_objects[$existing_translation_object->lid] = $existing_translation_object;
}
foreach ($form_state->getValue('strings') as $lid => $new_translation) {
$existing_translation = isset($existing_translation_objects[$lid]);
// Plural translations are saved in a delimited string. To be able to
// compare the new strings with the existing strings a string in the same
// format is created.
$new_translation_string_delimited = implode(LOCALE_PLURAL_DELIMITER, $new_translation['translations']);
// Generate an imploded string without delimiter, to be able to run
// empty() on it.
$new_translation_string = implode('', $new_translation['translations']);
$is_changed = FALSE;
if ($existing_translation && $existing_translation_objects[$lid]->translation != $new_translation_string_delimited) {
// If there is an existing translation in the DB and the new translation
// is not the same as the existing one.
$is_changed = TRUE;
}
elseif (!$existing_translation && !empty($new_translation_string)) {
// Newly entered translation.
$is_changed = TRUE;
}
if ($is_changed) {
// Only update or insert if we have a value to use.
$target = isset($existing_translation_objects[$lid]) ? $existing_translation_objects[$lid] : $this->localeStorage->createTranslation(array('lid' => $lid, 'language' => $langcode));
$target->setPlurals($new_translation['translations'])
->setCustomized()
->save();
$updated[] = $target->getId();
}
if (empty($new_translation_string) && isset($existing_translation_objects[$lid])) {
// Empty new translation entered: remove existing entry from database.
$existing_translation_objects[$lid]->delete();
$updated[] = $lid;
}
}
drupal_set_message($this->t('The strings have been saved.'));
// Keep the user on the current pager page.
$page = $this->getRequest()->query->get('page');
if (isset($page)) {
$form_state->setRedirect(
'locale.translate_page',
array(),
array('page' => $page)
);
}
if ($updated) {
// Clear cache and force refresh of JavaScript translations.
_locale_refresh_translations(array($langcode), $updated);
_locale_refresh_configuration(array($langcode), $updated);
}
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
* @file
* Contains \Drupal\locale\Form\TranslateFilterForm.
*/
namespace Drupal\locale\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a filtered translation edit form.
*/
class TranslateFilterForm extends TranslateFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'locale_translate_filter_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$filters = $this->translateFilters();
$filter_values = $this->translateFilterValues();
$form['#attached']['library'][] = 'locale/drupal.locale.admin';
$form['filters'] = array(
'#type' => 'details',
'#title' => $this->t('Filter translatable strings'),
'#open' => TRUE,
);
foreach ($filters as $key => $filter) {
// Special case for 'string' filter.
if ($key == 'string') {
$form['filters']['status']['string'] = array(
'#type' => 'search',
'#title' => $filter['title'],
'#description' => $filter['description'],
'#default_value' => $filter_values[$key],
);
}
else {
$empty_option = isset($filter['options'][$filter['default']]) ? $filter['options'][$filter['default']] : '- None -';
$form['filters']['status'][$key] = array(
'#title' => $filter['title'],
'#type' => 'select',
'#empty_value' => $filter['default'],
'#empty_option' => $empty_option,
'#size' => 0,
'#options' => $filter['options'],
'#default_value' => $filter_values[$key],
);
if (isset($filter['states'])) {
$form['filters']['status'][$key]['#states'] = $filter['states'];
}
}
}
$form['filters']['actions'] = array(
'#type' => 'actions',
'#attributes' => array('class' => array('container-inline')),
);
$form['filters']['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Filter'),
);
if (!empty($_SESSION['locale_translate_filter'])) {
$form['filters']['actions']['reset'] = array(
'#type' => 'submit',
'#value' => $this->t('Reset'),
'#submit' => array('::resetForm'),
);
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$filters = $this->translateFilters();
foreach ($filters as $name => $filter) {
if ($form_state->hasValue($name)) {
$_SESSION['locale_translate_filter'][$name] = $form_state->getValue($name);
}
}
$form_state->setRedirect('locale.translate_page');
}
/**
* Provides a submit handler for the reset button.
*/
public function resetForm(array &$form, FormStateInterface $form_state) {
$_SESSION['locale_translate_filter'] = array();
$form_state->setRedirect('locale.translate_page');
}
}

View file

@ -0,0 +1,219 @@
<?php
/**
* @file
* Contains \Drupal\locale\Form\TranslateFormBase.
*/
namespace Drupal\locale\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\locale\StringStorageInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the locale user interface translation form base.
*
* Provides methods for searching and filtering strings.
*/
abstract class TranslateFormBase extends FormBase {
/**
* The locale storage.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected $localeStorage;
/**
* The state store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/*
* Filter values. Shared between objects that inherit this class.
*
* @var array|null
*/
protected static $filterValues;
/**
* Constructs a new TranslationFormBase object.
*
* @param \Drupal\locale\StringStorageInterface $locale_storage
* The locale storage.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(StringStorageInterface $locale_storage, StateInterface $state, LanguageManagerInterface $language_manager) {
$this->localeStorage = $locale_storage;
$this->state = $state;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('locale.storage'),
$container->get('state'),
$container->get('language_manager')
);
}
/**
* Builds a string search query and returns an array of string objects.
*
* @return \Drupal\locale\TranslationString[]
* Array of \Drupal\locale\TranslationString objects.
*/
protected function translateFilterLoadStrings() {
$filter_values = $this->translateFilterValues();
// Language is sanitized to be one of the possible options in
// translateFilterValues().
$conditions = array('language' => $filter_values['langcode']);
$options = array('pager limit' => 30, 'translated' => TRUE, 'untranslated' => TRUE);
// Add translation status conditions and options.
switch ($filter_values['translation']) {
case 'translated':
$conditions['translated'] = TRUE;
if ($filter_values['customized'] != 'all') {
$conditions['customized'] = $filter_values['customized'];
}
break;
case 'untranslated':
$conditions['translated'] = FALSE;
break;
}
if (!empty($filter_values['string'])) {
$options['filters']['source'] = $filter_values['string'];
if ($options['translated']) {
$options['filters']['translation'] = $filter_values['string'];
}
}
return $this->localeStorage->getTranslations($conditions, $options);
}
/**
* Builds an array out of search criteria specified in request variables.
*
* @param bool $reset
* If the list of values should be reset.
*
* @return array
* The filter values.
*/
protected function translateFilterValues($reset = FALSE) {
if (!$reset && static::$filterValues) {
return static::$filterValues;
}
$filter_values = array();
$filters = $this->translateFilters();
foreach ($filters as $key => $filter) {
$filter_values[$key] = $filter['default'];
// Let the filter defaults be overwritten by parameters in the URL.
if ($this->getRequest()->query->has($key)) {
// Only allow this value if it was among the options, or
// if there were no fixed options to filter for.
$value = $this->getRequest()->query->get($key);
if (!isset($filter['options']) || isset($filter['options'][$value])) {
$filter_values[$key] = $value;
}
}
elseif (isset($_SESSION['locale_translate_filter'][$key])) {
// Only allow this value if it was among the options, or
// if there were no fixed options to filter for.
if (!isset($filter['options']) || isset($filter['options'][$_SESSION['locale_translate_filter'][$key]])) {
$filter_values[$key] = $_SESSION['locale_translate_filter'][$key];
}
}
}
return static::$filterValues = $filter_values;
}
/**
* Lists locale translation filters that can be applied.
*/
protected function translateFilters() {
$filters = array();
// Get all languages, except English.
$this->languageManager->reset();
$languages = $this->languageManager->getLanguages();
$language_options = array();
foreach ($languages as $langcode => $language) {
if (locale_is_translatable($langcode)) {
$language_options[$langcode] = $language->getName();
}
}
// Pick the current interface language code for the filter.
$default_langcode = $this->languageManager->getCurrentLanguage()->getId();
if (!isset($language_options[$default_langcode])) {
$available_langcodes = array_keys($language_options);
$default_langcode = array_shift($available_langcodes);
}
$filters['string'] = array(
'title' => $this->t('String contains'),
'description' => $this->t('Leave blank to show all strings. The search is case sensitive.'),
'default' => '',
);
$filters['langcode'] = array(
'title' => $this->t('Translation language'),
'options' => $language_options,
'default' => $default_langcode,
);
$filters['translation'] = array(
'title' => $this->t('Search in'),
'options' => array(
'all' => $this->t('Both translated and untranslated strings'),
'translated' => $this->t('Only translated strings'),
'untranslated' => $this->t('Only untranslated strings'),
),
'default' => 'all',
);
$filters['customized'] = array(
'title' => $this->t('Translation type'),
'options' => array(
'all' => $this->t('All'),
LOCALE_NOT_CUSTOMIZED => $this->t('Non-customized translation'),
LOCALE_CUSTOMIZED => $this->t('Customized translation'),
),
'states' => array(
'visible' => array(
':input[name=translation]' => array('value' => 'translated'),
),
),
'default' => 'all',
);
return $filters;
}
}

View file

@ -0,0 +1,309 @@
<?php
/**
* @file
* Contains \Drupal\locale\Form\TranslationStatusForm.
*/
namespace Drupal\locale\Form;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a translation status form.
*/
class TranslationStatusForm extends FormBase {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The Drupal state storage service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('state')
);
}
/**
* Constructs a TranslationStatusForm object.
*
* @param ModuleHandlerInterface $module_handler
* A module handler.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(ModuleHandlerInterface $module_handler, StateInterface $state) {
$this->moduleHandler = $module_handler;
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'locale_translation_status_form';
}
/**
* Form builder for displaying the current translation status.
*
* @ingroup forms
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$languages = locale_translatable_language_list();
$status = locale_translation_get_status();
$options = array();
$languages_update = array();
$languages_not_found = array();
$projects_update = array();
// Prepare information about projects which have available translation
// updates.
if ($languages && $status) {
$updates = $this->prepareUpdateData($status);
// Build data options for the select table.
foreach ($updates as $langcode => $update) {
$title = SafeMarkup::checkPlain($languages[$langcode]->getName());
$locale_translation_update_info = array('#theme' => 'locale_translation_update_info');
foreach (array('updates', 'not_found') as $update_status) {
if (isset($update[$update_status])) {
$locale_translation_update_info['#' . $update_status] = $update[$update_status];
}
}
$options[$langcode] = array(
'title' => array(
'class' => array('label'),
'data' => array(
'#title' => $title,
'#markup' => $title,
),
),
'status' => array(
'class' => array('description', 'priority-low'),
'data' => drupal_render($locale_translation_update_info),
),
);
if (!empty($update['not_found'])) {
$languages_not_found[$langcode] = $langcode;
}
if (!empty($update['updates'])) {
$languages_update[$langcode] = $langcode;
}
}
// Sort the table data on language name.
uasort($options, function ($a, $b) {
return strcasecmp($a['title']['data']['#title'], $b['title']['data']['#title']);
});
$languages_not_found = array_diff($languages_not_found, $languages_update);
}
$last_checked = $this->state->get('locale.translation_last_checked');
$form['last_checked'] = array(
'#theme' => 'locale_translation_last_check',
'#last' => $last_checked,
);
$header = array(
'title' => array(
'data' => $this->t('Language'),
'class' => array('title'),
),
'status' => array(
'data' => $this->t('Status'),
'class' => array('status', 'priority-low'),
),
);
if (!$languages) {
$empty = $this->t('No translatable languages available. <a href="@add_language">Add a language</a> first.', array(
'@add_language' => $this->url('entity.configurable_language.collection'),
));
}
elseif ($status) {
$empty = $this->t('All translations up to date.');
}
else {
$empty = $this->t('No translation status available. <a href="@check">Check manually</a>.', array(
'@check' => $this->url('locale.check_translation'),
));
}
// The projects which require an update. Used by the _submit callback.
$form['projects_update'] = array(
'#type' => 'value',
'#value' => $projects_update,
);
$form['langcodes'] = array(
'#type' => 'tableselect',
'#header' => $header,
'#options' => $options,
'#default_value' => $languages_update,
'#empty' => $empty,
'#js_select' => TRUE,
'#multiple' => TRUE,
'#required' => TRUE,
'#not_found' => $languages_not_found,
'#after_build' => array('locale_translation_language_table'),
);
$form['#attached']['library'][] = 'locale/drupal.locale.admin';
$form['actions'] = array('#type' => 'actions');
if ($languages_update) {
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Update translations'),
);
}
return $form;
}
/**
* Prepare information about projects with available translation updates.
*
* @param array $status
* Translation update status as an array keyed by Project ID and langcode.
*
* @return array
* Translation update status as an array keyed by language code and
* translation update status.
*/
protected function prepareUpdateData(array $status) {
$updates = array();
// @todo Calling locale_translation_build_projects() is an expensive way to
// get a module name. In follow-up issue
// https://www.drupal.org/node/1842362 the project name will be stored to
// display use, like here.
$this->moduleHandler->loadInclude('locale', 'compare.inc');
$project_data = locale_translation_build_projects();
foreach ($status as $project_id => $project) {
foreach ($project as $langcode => $project_info) {
// No translation file found for this project-language combination.
if (empty($project_info->type)) {
$updates[$langcode]['not_found'][] = array(
'name' => $project_info->name == 'drupal' ? $this->t('Drupal core') : $project_data[$project_info->name]->info['name'],
'version' => $project_info->version,
'info' => $this->createInfoString($project_info),
);
}
// Translation update found for this project-language combination.
elseif ($project_info->type == LOCALE_TRANSLATION_LOCAL || $project_info->type == LOCALE_TRANSLATION_REMOTE) {
$local = isset($project_info->files[LOCALE_TRANSLATION_LOCAL]) ? $project_info->files[LOCALE_TRANSLATION_LOCAL] : NULL;
$remote = isset($project_info->files[LOCALE_TRANSLATION_REMOTE]) ? $project_info->files[LOCALE_TRANSLATION_REMOTE] : NULL;
$recent = _locale_translation_source_compare($local, $remote) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $remote : $local;
$updates[$langcode]['updates'][] = array(
'name' => $project_info->name == 'drupal' ? $this->t('Drupal core') : $project_data[$project_info->name]->info['name'],
'version' => $project_info->version,
'timestamp' => $recent->timestamp,
);
}
}
}
return $updates;
}
/**
* Provides debug info for projects in case translation files are not found.
*
* Translations files are being fetched either from Drupal translation server
* and local files or only from the local filesystem depending on the
* "Translation source" setting at admin/config/regional/translate/settings.
* This method will produce debug information including the respective path(s)
* based on this setting.
*
* Translations for development versions are never fetched, so the debug info
* for that is a fixed message.
*
* @param array $project_info
* An array which is the project information of the source.
*
* @return string
* The string which contains debug information.
*/
protected function createInfoString($project_info) {
$remote_path = isset($project_info->files['remote']->uri) ? $project_info->files['remote']->uri : FALSE;
$local_path = isset($project_info->files['local']->uri) ? $project_info->files['local']->uri : FALSE;
if (strpos($project_info->version, 'dev') !== FALSE) {
return $this->t('No translation files are provided for development releases.');
}
if (locale_translation_use_remote_source() && $remote_path && $local_path) {
return $this->t('File not found at %remote_path nor at %local_path', array(
'%remote_path' => $remote_path,
'%local_path' => $local_path,
));
}
elseif ($local_path) {
return $this->t('File not found at %local_path', array('%local_path' => $local_path));
}
return $this->t('Translation file location could not be determined.');
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Check if a language has been selected. 'tableselect' doesn't.
if (!array_filter($form_state->getValue('langcodes'))) {
$form_state->setErrorByName('', $this->t('Select a language to update.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->moduleHandler->loadInclude('locale', 'fetch.inc');
$this->moduleHandler->loadInclude('locale', 'bulk.inc');
$langcodes = array_filter($form_state->getValue('langcodes'));
$projects = array_filter($form_state->getValue('projects_update'));
// Set the translation import options. This determines if existing
// translations will be overwritten by imported strings.
$options = _locale_translation_default_update_options();
// If the status was updated recently we can immediately start fetching the
// translation updates. If the status is expired we clear it an run a batch to
// update the status and then fetch the translation updates.
$last_checked = $this->state->get('locale.translation_last_checked');
if ($last_checked < REQUEST_TIME - LOCALE_TRANSLATION_STATUS_TTL) {
locale_translation_clear_status();
$batch = locale_translation_batch_update_build(array(), $langcodes, $options);
batch_set($batch);
}
else {
// Set a batch to download and import translations.
$batch = locale_translation_batch_fetch_build($projects, $langcodes, $options);
batch_set($batch);
// Set a batch to update configuration as well.
if ($batch = locale_config_batch_update_components($options, $langcodes)) {
batch_set($batch);
}
}
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* @file
* Contains \Drupal\locale\Gettext.
*/
namespace Drupal\locale;
use Drupal\Component\Gettext\PoStreamReader;
use Drupal\Component\Gettext\PoMemoryWriter;
use Drupal\locale\PoDatabaseWriter;
/**
* Static class providing Drupal specific Gettext functionality.
*
* The operations are related to pumping data from a source to a destination,
* for example:
* - Remote files http://*.po to memory
* - File public://*.po to database
*/
class Gettext {
/**
* Reads the given PO files into the database.
*
* @param object $file
* File object with an URI property pointing at the file's path.
* - "langcode": The language the strings will be added to.
* - "uri": File URI.
* @param array $options
* An array with options that can have the following elements:
* - 'overwrite_options': Overwrite options array as defined in
* Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
* are customized translations or come from a community source. Use
* LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
* LOCALE_NOT_CUSTOMIZED.
* - 'seek': Specifies from which position in the file should the reader
* start reading the next items. Optional, defaults to 0.
* - 'items': Specifies the number of items to read. Optional, defaults to
* -1, which means that all the items from the stream will be read.
*
* @return array
* Report array as defined in Drupal\locale\PoDatabaseWriter.
*
* @see \Drupal\locale\PoDatabaseWriter
*/
public static function fileToDatabase($file, $options) {
// Add the default values to the options array.
$options += array(
'overwrite_options' => array(),
'customized' => LOCALE_NOT_CUSTOMIZED,
'items' => -1,
'seek' => 0,
);
// Instantiate and initialize the stream reader for this file.
$reader = new PoStreamReader();
$reader->setLangcode($file->langcode);
$reader->setURI($file->uri);
try {
$reader->open();
}
catch (\Exception $exception) {
throw $exception;
}
$header = $reader->getHeader();
if (!$header) {
throw new \Exception('Missing or malformed header.');
}
// Initialize the database writer.
$writer = new PoDatabaseWriter();
$writer->setLangcode($file->langcode);
$writer_options = array(
'overwrite_options' => $options['overwrite_options'],
'customized' => $options['customized'],
);
$writer->setOptions($writer_options);
$writer->setHeader($header);
// Attempt to pipe all items from the file to the database.
try {
if ($options['seek']) {
$reader->setSeek($options['seek']);
}
$writer->writeItems($reader, $options['items']);
}
catch (\Exception $exception) {
throw $exception;
}
// Report back with an array of status information.
$report = $writer->getReport();
// Add the seek position to the report. This is useful for the batch
// operation.
$report['seek'] = $reader->getSeek();
return $report;
}
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\locale\Locale.
*/
namespace Drupal\locale;
/**
* Static service container wrapper for locale.
*/
class Locale {
/**
* Returns the locale configuration manager service.
*
* Use the locale config manager service for creating locale-wrapped typed
* configuration objects.
*
* @see \Drupal\Core\TypedData\TypedDataManager::create()
*
* @return \Drupal\locale\LocaleConfigManager
*/
public static function config() {
return \Drupal::service('locale.config_manager');
}
}

View file

@ -0,0 +1,638 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleConfigManager.
*/
namespace Drupal\locale;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\language\ConfigurableLanguageManagerInterface;
/**
* Manages configuration supported in part by interface translation.
*
* This manager is responsible to update configuration overrides and active
* translations when interface translation data changes. This allows Drupal to
* translate user roles, views, blocks, etc. after Drupal has been installed
* using the locale module's storage. When translations change in locale,
* LocaleConfigManager::updateConfigTranslations() is invoked to update the
* corresponding storage of the translation in the original config object or an
* override.
*
* In turn when translated configuration or configuration language overrides are
* changed, it is the responsibility of LocaleConfigSubscriber to update locale
* storage.
*
* By design locale module only deals with sources in English.
*
* @see \Drupal\locale\LocaleConfigSubscriber
*/
class LocaleConfigManager {
/**
* The storage instance for reading configuration data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* The string storage for reading and writing translations.
*
* @var \Drupal\locale\StringStorageInterface;
*/
protected $localeStorage;
/**
* Array with preloaded string translations.
*
* @var array
*/
protected $translations;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The language manager.
*
* @var \Drupal\language\ConfigurableLanguageManagerInterface
*/
protected $languageManager;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
/**
* Whether or not configuration translations are being updated from locale.
*
* @see self::isUpdatingFromLocale()
*
* @var bool
*/
protected $isUpdatingFromLocale = FALSE;
/**
* The locale default config storage instance.
*
* @var \Drupal\locale\LocaleDefaultConfigStorage
*/
protected $defaultConfigStorage;
/**
* Creates a new typed configuration manager.
*
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The storage object to use for reading configuration data.
* @param \Drupal\locale\StringStorageInterface $locale_storage
* The locale storage to use for reading string translations.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* The typed configuration manager.
* @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\locale\LocaleDefaultConfigStorage $default_config_storage
* The locale default configuration storage.
*/
public function __construct(StorageInterface $config_storage, StringStorageInterface $locale_storage, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config, ConfigurableLanguageManagerInterface $language_manager, LocaleDefaultConfigStorage $default_config_storage) {
$this->configStorage = $config_storage;
$this->localeStorage = $locale_storage;
$this->configFactory = $config_factory;
$this->typedConfigManager = $typed_config;
$this->languageManager = $language_manager;
$this->defaultConfigStorage = $default_config_storage;
}
/**
* Gets array of translation wrappers for translatable configuration.
*
* @param string $name
* Configuration object name.
*
* @return array
* Array of translatable elements of the default configuration in $name.
*/
public function getTranslatableDefaultConfig($name) {
if ($this->isSupported($name)) {
// Create typed configuration wrapper based on install storage data.
$data = $this->defaultConfigStorage->read($name);
$type_definition = $this->typedConfigManager->getDefinition($name);
$data_definition = $this->typedConfigManager->buildDataDefinition($type_definition, $data);
$typed_config = $this->typedConfigManager->create($data_definition, $data);
if ($typed_config instanceof TraversableTypedDataInterface) {
return $this->getTranslatableData($typed_config);
}
}
return array();
}
/**
* Gets translatable configuration data for a typed configuration element.
*
* @param \Drupal\Core\TypedData\TypedDataInterface $element
* Typed configuration element.
*
* @return array|\Drupal\Core\StringTranslation\TranslationWrapper
* A nested array matching the exact structure under $element with only the
* elements that are translatable wrapped into a TranslationWrapper. If the
* provided $element is not traversable, the return value is a single
* TranslationWrapper.
*/
protected function getTranslatableData(TypedDataInterface $element) {
$translatable = array();
if ($element instanceof TraversableTypedDataInterface) {
foreach ($element as $key => $property) {
$value = $this->getTranslatableData($property);
if (!empty($value)) {
$translatable[$key] = $value;
}
}
}
else {
$definition = $element->getDataDefinition();
if (!empty($definition['translatable'])) {
$options = array();
if (isset($definition['translation context'])) {
$options['context'] = $definition['translation context'];
}
return new TranslationWrapper($element->getValue(), array(), $options);
}
}
return $translatable;
}
/**
* Process the translatable data array with a given language.
*
* If the given language is translatable, will return the translated copy
* which will only contain strings that had translations. If the given
* language is English and is not translatable, will return a simplified
* array of the English source strings only.
*
* @param string $name
* The configuration name.
* @param array $active
* The active configuration data.
* @param array|\Drupal\Core\StringTranslation\TranslationWrapper[] $translatable
* The translatable array structure. A nested array matching the exact
* structure under of the default configuration for $name with only the
* elements that are translatable wrapped into a TranslationWrapper.
* @see self::getTranslatableData().
* @param string $langcode
* The language code to process the array with.
*
* @return array
* Processed translatable data array. Will only contain translations
* different from source strings or in case of untranslatable English, the
* source strings themselves.
*/
protected function processTranslatableData($name, array $active, array $translatable, $langcode) {
$translated = array();
foreach ($translatable as $key => $item) {
if (!isset($active[$key])) {
continue;
}
if (is_array($item)) {
// Only add this key if there was a translated value underneath.
$value = $this->processTranslatableData($name, $active[$key], $item, $langcode);
if (!empty($value)) {
$translated[$key] = $value;
}
}
else {
if (locale_is_translatable($langcode)) {
$value = $this->translateString($name, $langcode, $item->getUntranslatedString(), $item->getOption('context'));
}
else {
$value = $item->getUntranslatedString();
}
if (!empty($value)) {
$translated[$key] = $value;
}
}
}
return $translated;
}
/**
* Saves translated configuration override.
*
* @param string $name
* Configuration object name.
* @param string $langcode
* Language code.
* @param array $data
* Configuration data to be saved, that will be only the translated values.
*/
protected function saveTranslationOverride($name, $langcode, array $data) {
$this->isUpdatingFromLocale = TRUE;
$this->languageManager->getLanguageConfigOverride($langcode, $name)->setData($data)->save();
$this->isUpdatingFromLocale = FALSE;
}
/**
* Saves translated configuration data.
*
* @param string $name
* Configuration object name.
* @param array $data
* Configuration data to be saved with translations merged in.
*/
protected function saveTranslationActive($name, array $data) {
$this->isUpdatingFromLocale = TRUE;
$this->configFactory->getEditable($name)->setData($data)->save();
$this->isUpdatingFromLocale = FALSE;
}
/**
* Deletes translated configuration data.
*
* @param string $name
* Configuration object name.
* @param string $langcode
* Language code.
*/
protected function deleteTranslationOverride($name, $langcode) {
$this->isUpdatingFromLocale = TRUE;
$this->languageManager->getLanguageConfigOverride($langcode, $name)->delete();
$this->isUpdatingFromLocale = FALSE;
}
/**
* Gets configuration names associated with components.
*
* @param array $components
* (optional) Array of component lists indexed by type. If not present or it
* is an empty array, it will update all components.
*
* @return array
* Array of configuration object names.
*/
public function getComponentNames(array $components = array()) {
$components = array_filter($components);
if ($components) {
$names = array();
foreach ($components as $type => $list) {
// InstallStorage::getComponentNames returns a list of folders keyed by
// config name.
$names = array_merge($names, $this->defaultConfigStorage->getComponentNames($type, $list));
}
return $names;
}
else {
return $this->defaultConfigStorage->listAll();
}
}
/**
* Gets configuration names associated with strings.
*
* @param array $lids
* Array with string identifiers.
*
* @return array
* Array of configuration object names.
*/
public function getStringNames(array $lids) {
$names = array();
$locations = $this->localeStorage->getLocations(array('sid' => $lids, 'type' => 'configuration'));
foreach ($locations as $location) {
$names[$location->name] = $location->name;
}
return $names;
}
/**
* Deletes configuration for language.
*
* @param string $langcode
* Language code to delete.
*/
public function deleteLanguageTranslations($langcode) {
$this->isUpdatingFromLocale = TRUE;
$storage = $this->languageManager->getLanguageConfigOverrideStorage($langcode);
foreach ($storage->listAll() as $name) {
$this->languageManager->getLanguageConfigOverride($langcode, $name)->delete();
}
$this->isUpdatingFromLocale = FALSE;
}
/**
* Translates string using the localization system.
*
* So far we only know how to translate strings from English so the source
* string should be in English.
* Unlike regular t() translations, strings will be added to the source
* tables only if this is marked as default data.
*
* @param string $name
* Name of the configuration location.
* @param string $langcode
* Language code to translate to.
* @param string $source
* The source string, should be English.
* @param string $context
* The string context.
*
* @return string|false
* Translated string if there is a translation, FALSE if not.
*/
public function translateString($name, $langcode, $source, $context) {
if ($source) {
// If translations for a language have not been loaded yet.
if (!isset($this->translations[$name][$langcode])) {
// Preload all translations for this configuration name and language.
$this->translations[$name][$langcode] = array();
foreach ($this->localeStorage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => $name)) as $string) {
$this->translations[$name][$langcode][$string->context][$string->source] = $string;
}
}
if (!isset($this->translations[$name][$langcode][$context][$source])) {
// There is no translation of the source string in this config location
// to this language for this context.
if ($translation = $this->localeStorage->findTranslation(array('source' => $source, 'context' => $context, 'language' => $langcode))) {
// Look for a translation of the string. It might have one, but not
// be saved in this configuration location yet.
// If the string has a translation for this context to this language,
// save it in the configuration location so it can be looked up faster
// next time.
$this->localeStorage->createString((array) $translation)
->addLocation('configuration', $name)
->save();
}
else {
// No translation was found. Add the source to the configuration
// location so it can be translated, and the string is faster to look
// for next time.
$translation = $this->localeStorage
->createString(array('source' => $source, 'context' => $context))
->addLocation('configuration', $name)
->save();
}
// Add an entry, either the translation found, or a blank string object
// to track the source string, to this configuration location, language,
// and context.
$this->translations[$name][$langcode][$context][$source] = $translation;
}
// Return the string only when the string object had a translation.
if ($this->translations[$name][$langcode][$context][$source]->isTranslation()) {
return $this->translations[$name][$langcode][$context][$source]->getString();
}
}
return FALSE;
}
/**
* Reset static cache of configuration string translations.
*
* @return $this
*/
public function reset() {
$this->translations = array();
return $this;
}
/**
* Get the translation object for the given source/context and language.
*
* @param string $name
* Name of the configuration location.
* @param string $langcode
* Language code to translate to.
* @param string $source
* The source string, should be English.
* @param string $context
* The string context.
*
* @return \Drupal\locale\TranslationString|FALSE
* The translation object if the string was not empty or FALSE otherwise.
*/
public function getStringTranslation($name, $langcode, $source, $context) {
if ($source) {
$this->translateString($name, $langcode, $source, $context);
if ($string = $this->translations[$name][$langcode][$context][$source]) {
if (!$string->isTranslation()) {
$conditions = array('lid' => $string->lid, 'language' => $langcode);
$translation = $this->localeStorage->createTranslation($conditions);
$this->translations[$name][$langcode][$context][$source] = $translation;
return $translation;
}
else {
return $string;
}
}
}
return FALSE;
}
/**
* Checks whether a language has configuration translation.
*
* @param string $name
* Configuration name.
* @param string $langcode
* A language code.
*
* @return bool
* A boolean indicating if a language has configuration translations.
*/
public function hasTranslation($name, $langcode) {
$translation = $this->languageManager->getLanguageConfigOverride($langcode, $name);
return !$translation->isNew();
}
/**
* Returns the original language code for this shipped configuration.
*
* @param string $name
* The configuration name.
*
* @return null|string
* Language code of the default configuration for $name. If the default
* configuration data for $name did not contain a language code, it is
* assumed to be English. The return value is NULL if no such default
* configuration exists.
*/
public function getDefaultConfigLangcode($name) {
$shipped = $this->defaultConfigStorage->read($name);
if (!empty($shipped)) {
return !empty($shipped['langcode']) ? $shipped['langcode'] : 'en';
}
}
/**
* Returns the current language code for this active configuration.
*
* @param string $name
* The configuration name.
*
* @return null|string
* Language code of the current active configuration for $name. If the
* configuration data for $name did not contain a language code, it is
* assumed to be English. The return value is NULL if no such active
* configuration exists.
*/
public function getActiveConfigLangcode($name) {
$active = $this->configStorage->read($name);
if (!empty($active)) {
return !empty($active['langcode']) ? $active['langcode'] : 'en';
}
}
/**
* Whether the given configuration is supported for interface translation.
*
* @param string $name
* The configuration name.
*
* @return bool
* TRUE if interface translation is supported.
*/
public function isSupported($name) {
return $this->getDefaultConfigLangcode($name) == 'en' && $this->configStorage->read($name);
}
/**
* Indicates whether configuration translations are being updated from locale.
*
* @return bool
* Whether or not configuration translations are currently being updated.
* If TRUE, LocaleConfigManager is in control of the process and the
* reference data is locale's storage. Changes made to active configuration
* and overrides in this case should not feed back to locale storage.
* On the other hand, when not updating from locale and configuration
* translations change, we need to feed back to the locale storage.
*/
public function isUpdatingTranslationsFromLocale() {
return $this->isUpdatingFromLocale;
}
/**
* Updates all configuration translations for the names / languages provided.
*
* To be used when interface translation changes result in the need to update
* configuration translations to keep them in sync.
*
* @param array $names
* Array of names of configuration objects to update.
* @param array $langcodes
* (optional) Array of language codes to update. Defaults to all
* configurable languages.
*
* @return int
* Total number of configuration override and active configuration objects
* updated (saved or removed).
*/
public function updateConfigTranslations(array $names, array $langcodes = array()) {
$langcodes = $langcodes ? $langcodes : array_keys($this->languageManager->getLanguages());
$count = 0;
foreach ($names as $name) {
$translatable = $this->getTranslatableDefaultConfig($name);
if (empty($translatable)) {
// If there is nothing translatable in this configuration or not
// supported, skip it.
continue;
}
$active_langcode = $this->getActiveConfigLangcode($name);
$active = $this->configStorage->read($name);
foreach ($langcodes as $langcode) {
$processed = $this->processTranslatableData($name, $active, $translatable, $langcode);
if ($langcode != $active_langcode) {
// If the language code is not the same as the active storage
// language, we should update a configuration override.
if (!empty($processed)) {
// Update translation data in configuration override.
$this->saveTranslationOverride($name, $langcode, $processed);
$count++;
}
else {
$override = $this->languageManager->getLanguageConfigOverride($langcode, $name);
if (!$override->isNew()) {
$data = $this->filterOverride($override->get(), $translatable);
if (empty($data)) {
// Delete language override if there is no data left at all.
// This means all prior translations in the override were locale
// managed.
$this->deleteTranslationOverride($name, $langcode);
$count++;
}
else {
// If there were translatable elements besides locale managed
// items, save with only those, and remove the ones managed
// by locale only.
$this->saveTranslationOverride($name, $langcode, $data);
$count++;
}
}
}
}
elseif (locale_is_translatable($langcode)) {
// If the language code is the active storage language, we should
// update. If it is English, we should only update if English is also
// translatable.
$active = NestedArray::mergeDeepArray(array($active, $processed), TRUE);
$this->saveTranslationActive($name, $active);
$count++;
}
}
}
return $count;
}
/**
* Filters override data based on default translatable items.
*
* @param array $override_data
* Configuration override data.
* @param array $translatable
* Translatable data array. @see self::getTranslatableData()
* @return array
* Nested array of any items of $override_data which did not have keys in
* $translatable. May be empty if $override_data only had items which were
* also in $translatable.
*/
protected function filterOverride(array $override_data, array $translatable) {
$filtered_data = array();
foreach ($override_data as $key => $value) {
if (isset($translatable[$key])) {
// If the translatable default configuration has this key, look further
// for subkeys or ignore this element for scalar values.
if (is_array($value)) {
$value = $this->filterOverride($value, $translatable[$key]);
if (!empty($value)) {
$filtered_data[$key] = $value;
}
}
}
else {
// If this key was not in the translatable default configuration,
// keep it.
$filtered_data[$key] = $value;
}
}
return $filtered_data;
}
}

View file

@ -0,0 +1,232 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleConfigSubscriber.
*/
namespace Drupal\locale;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\StorableConfigBase;
use Drupal\language\Config\LanguageConfigOverrideCrudEvent;
use Drupal\language\Config\LanguageConfigOverrideEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Updates strings translation when configuration translations change.
*
* This reacts to the updates of translated active configuration and
* configuration language overrides. When those updates involve configuration
* which was available as default configuration, we need to feed back changes
* to any item which was originally part of that configuration to the interface
* translation storage. Those updated translations are saved as customized, so
* further community translation updates will not undo user changes.
*
* This subscriber does not respond to deleting active configuration or deleting
* configuration translations. The locale storage is additive and we cannot be
* sure that only a given configuration translation used a source string. So
* we should not remove the translations from locale storage in these cases. The
* configuration or override would itself be deleted either way.
*
* By design locale module only deals with sources in English.
*
* @see \Drupal\locale\LocaleConfigManager
*/
class LocaleConfigSubscriber implements EventSubscriberInterface {
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The typed configuration manager.
*
* @var \Drupal\locale\LocaleConfigManager
*/
protected $localeConfigManager;
/**
* Constructs a LocaleConfigSubscriber.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\locale\LocaleConfigManager $locale_config_manager
* The typed configuration manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager) {
$this->configFactory = $config_factory;
$this->localeConfigManager = $locale_config_manager;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[LanguageConfigOverrideEvents::SAVE_OVERRIDE] = 'onOverrideChange';
$events[LanguageConfigOverrideEvents::DELETE_OVERRIDE] = 'onOverrideChange';
$events[ConfigEvents::SAVE] = 'onConfigSave';
return $events;
}
/**
* Updates the locale strings when a translated active configuration is saved.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigSave(ConfigCrudEvent $event) {
// Only attempt to feed back configuration translation changes to locale if
// the update itself was not initiated by locale data changes.
if (!drupal_installation_attempted() && !$this->localeConfigManager->isUpdatingTranslationsFromLocale()) {
$config = $event->getConfig();
$langcode = $config->get('langcode') ?: 'en';
$this->updateLocaleStorage($config, $langcode);
}
}
/**
* Updates the locale strings when a configuration override is saved/deleted.
*
* @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event
* The language configuration event.
*/
public function onOverrideChange(LanguageConfigOverrideCrudEvent $event) {
// Only attempt to feed back configuration override changes to locale if
// the update itself was not initiated by locale data changes.
if (!drupal_installation_attempted() && !$this->localeConfigManager->isUpdatingTranslationsFromLocale()) {
$translation_config = $event->getLanguageConfigOverride();
$langcode = $translation_config->getLangcode();
$reference_config = $this->configFactory->getEditable($translation_config->getName())->get();
$this->updateLocaleStorage($translation_config, $langcode, $reference_config);
}
}
/**
* Update locale storage based on configuration translations.
*
* @param \Drupal\Core\Config\StorableConfigBase $config
* Active configuration or configuration translation override.
* @param string $langcode
* The language code of $config.
* @param array $reference_config
* (Optional) Reference configuration to check against if $config was an
* override. This allows us to update locale keys for data not in the
* override but still in the active configuration.
*/
protected function updateLocaleStorage(StorableConfigBase $config, $langcode, array $reference_config = array()) {
$name = $config->getName();
if ($this->localeConfigManager->isSupported($name) && locale_is_translatable($langcode)) {
$translatables = $this->localeConfigManager->getTranslatableDefaultConfig($name);
$this->processTranslatableData($name, $config->get(), $translatables, $langcode, $reference_config);
}
}
/**
* Process the translatable data array with a given language.
*
* @param string $name
* The configuration name.
* @param array $config
* The active configuration data or override data.
* @param array|\Drupal\Core\StringTranslation\TranslationWrapper[] $translatable
* The translatable array structure.
* @see \Drupal\locale\LocaleConfigManager::getTranslatableData()
* @param string $langcode
* The language code to process the array with.
* @param array $reference_config
* (Optional) Reference configuration to check against if $config was an
* override. This allows us to update locale keys for data not in the
* override but still in the active configuration.
*/
protected function processTranslatableData($name, array $config, array $translatable, $langcode, array $reference_config = array()) {
foreach ($translatable as $key => $item) {
if (!isset($config[$key])) {
if (isset($reference_config[$key])) {
$this->resetExistingTranslations($name, $translatable[$key], $reference_config[$key], $langcode);
}
continue;
}
if (is_array($item)) {
$reference_config = isset($reference_config[$key]) ? $reference_config[$key] : array();
$this->processTranslatableData($name, $config[$key], $item, $langcode, $reference_config);
}
else {
$this->saveCustomizedTranslation($name, $item->getUntranslatedString(), $item->getOption('context'), $config[$key], $langcode);
}
}
}
/**
* Reset existing locale translations to their source values.
*
* Goes through $translatable to reset any existing translations to the source
* string, so prior translations would not reappear in the configuration.
*
* @param string $name
* The configuration name.
* @param array|\Drupal\Core\StringTranslation\TranslationWrapper $translatable
* Either a possibly nested array with TranslationWrapper objects at the
* leaf items or a TranslationWrapper object directly.
* @param array|string $reference_config
* Either a possibly nested array with strings at the leaf items or a string
* directly. Only those $translatable items that are also present in
* $reference_config will get translations reset.
* @param string $langcode
* The language code of the translation being processed.
*/
protected function resetExistingTranslations($name, $translatable, $reference_config, $langcode) {
if (is_array($translatable)) {
foreach ($translatable as $key => $item) {
if (isset($reference_config[$key])) {
// Process further if the key still exists in the reference active
// configuration and the default translation but not the current
// configuration override.
$this->resetExistingTranslations($name, $item, $reference_config[$key], $langcode);
}
}
}
elseif (!is_array($reference_config)) {
$this->saveCustomizedTranslation($name, $translatable->getUntranslatedString(), $translatable->getOption('context'), $reference_config, $langcode);
}
}
/**
* Saves a translation string and marks it as customized.
*
* @param string $name
* The configuration name.
* @param string $source
* The source string value.
* @param string $context
* The source string context.
* @param string $new_translation
* The translation string.
* @param string $langcode
* The language code of the translation.
*/
protected function saveCustomizedTranslation($name, $source, $context, $new_translation, $langcode) {
$locale_translation = $this->localeConfigManager->getStringTranslation($name, $langcode, $source, $context);
if (!empty($locale_translation)) {
// Save this translation as custom if it was a new translation and not the
// same as the source. (The interface prefills translation values with the
// source). Or if there was an existing (non-empty) translation and the
// user changed it (even if it was changed back to the original value).
// Otherwise the translation file would be overwritten with the locale
// copy again later.
$existing_translation = $locale_translation->getString();
if (($locale_translation->isNew() && $source != $new_translation) ||
(!$locale_translation->isNew() && ((empty($existing_translation) && $source != $new_translation) || ((!empty($existing_translation) && $new_translation != $existing_translation))))) {
$locale_translation
->setString($new_translation)
->setCustomized(TRUE)
->save();
}
}
}
}

View file

@ -0,0 +1,165 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleDefaultConfigStorage.
*/
namespace Drupal\locale;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\language\ConfigurableLanguageManagerInterface;
/**
* Provides access to default configuration for locale integration.
*
* Allows unified access to default configuration from one of three sources:
* - Required default configuration (config/install/*)
* - Optional default configuration (config/optional/*)
* - Predefined languages mocked as default configuration (list defined in
* LocaleConfigManagerInterface::getStandardLanguageList())
*
* These sources are considered equal in terms of how locale module interacts
* with them for translation. Their translatable source strings are exposed
* for interface translation and participate in remote translation updates.
*/
class LocaleDefaultConfigStorage {
/**
* The storage instance for reading configuration data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* The language manager.
*
* @var \Drupal\language\ConfigurableLanguageManagerInterface
*/
protected $languageManager;
/**
* The storage instance for reading required default configuration data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $requiredInstallStorage;
/**
* The storage instance for reading optional default configuration data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $optionalInstallStorage;
/**
* Constructs a LocaleDefaultConfigStorage.
*
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The storage object to use for reading configuration data.
* @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(StorageInterface $config_storage, ConfigurableLanguageManagerInterface $language_manager) {
$this->configStorage = $config_storage;
$this->languageManager = $language_manager;
$this->requiredInstallStorage = new InstallStorage();
$this->optionalInstallStorage = new InstallStorage(InstallStorage::CONFIG_OPTIONAL_DIRECTORY);
}
/**
* Read a configuration from install storage or default languages.
*
* @param string $name
* Configuration object name.
*
* @return array
* Configuration data from install storage or default language.
*/
public function read($name) {
if ($this->requiredInstallStorage->exists($name)) {
return $this->requiredInstallStorage->read($name);
}
elseif ($this->optionalInstallStorage->exists($name)) {
return $this->optionalInstallStorage->read($name);
}
elseif (strpos($name, 'language.entity.') === 0) {
// Simulate default languages as if they were shipped as default
// configuration.
$langcode = str_replace('language.entity.', '', $name);
$predefined_languages = $this->languageManager->getStandardLanguageList();
if (isset($predefined_languages[$langcode])) {
$data = $this->configStorage->read($name);
$data['label'] = $predefined_languages[$langcode][0];
return $data;
}
}
}
/**
* Return the list of configuration in install storage and current languages.
*
* @return array
* List of configuration in install storage and current languages.
*/
public function listAll() {
$languages = $this->predefinedConfiguredLanguages();
return array_unique(
array_merge(
$this->requiredInstallStorage->listAll(),
$this->optionalInstallStorage->listAll(),
$languages
)
);
}
/**
* Get all configuration names and folders for a list of modules or themes.
*
* @param string $type
* Type of components: 'module' | 'theme' | 'profile'
* @param array $list
* Array of theme or module names.
*
* @return array
* Configuration names provided by that component. In case of language
* module this list is extended with configured languages that have
* predefined names as well.
*/
public function getComponentNames($type, array $list) {
$names = array_unique(
array_merge(
array_keys($this->requiredInstallStorage->getComponentNames($type, $list)),
array_keys($this->optionalInstallStorage->getComponentNames($type, $list))
)
);
if ($type == 'module' && in_array('language', $list)) {
$languages = $this->predefinedConfiguredLanguages();
$names = array_unique(array_merge($names, $languages));
}
return $names;
}
/**
* Compute the list of configuration names that match predefined languages.
*
* @return array
* The list of configuration names that match predefined languages.
*/
protected function predefinedConfiguredLanguages() {
$names = $this->configStorage->listAll('language.entity.');
$predefined_languages = $this->languageManager->getStandardLanguageList();
foreach ($names as $id => $name) {
$langcode = str_replace('language.entity.', '', $name);
if (!isset($predefined_languages[$langcode])) {
unset($names[$id]);
}
}
return array_values($names);
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleEvent.
*/
namespace Drupal\locale;
use Symfony\Component\EventDispatcher\Event;
/**
* Defines a Locale event.
*/
class LocaleEvent extends Event {
/**
* The list of Language codes for updated translations.
*
* @var string[]
*/
protected $langCodes;
/**
* List of string identifiers that have been updated / created.
*
* @var string[]
*/
protected $original;
/**
* Constructs a new LocaleEvent.
*
* @param array $lang_codes
* Language codes for updated translations.
* @param array $lids
* (optional) List of string identifiers that have been updated / created.
*/
public function __construct(array $lang_codes, array $lids = array()) {
$this->langCodes = $lang_codes;
$this->lids = $lids;
}
/**
* Returns the language codes.
*
* @return string[] $langCodes
*/
public function getLangCodes() {
return $this->langCodes;
}
/**
* Returns the string identifiers.
*
* @return array $lids
*/
public function getLids() {
return $this->lids;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleEvents.
*/
namespace Drupal\locale;
/**
* Defines events for locale translation.
*
* @see \Drupal\Core\Config\ConfigCrudEvent
*/
final class LocaleEvents {
/**
* The name of the event fired when saving a translated string.
*
* This event allows you to perform custom actions whenever a translated
* string is saved.
*
* @Event
*
* @see \Drupal\locale\EventSubscriber\LocaleTranslationCacheTag
*/
const SAVE_TRANSLATION = 'locale.save_translation';
}

View file

@ -0,0 +1,191 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleLookup.
*/
namespace Drupal\locale;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* A cache collector to allow for dynamic building of the locale cache.
*/
class LocaleLookup extends CacheCollector {
/**
* A language code.
*
* @var string
*/
protected $langcode;
/**
* The msgctxt context.
*
* @var string
*/
protected $context;
/**
* The locale storage.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected $stringStorage;
/**
* The cache backend that should be used.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The lock backend that should be used.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a LocaleLookup object.
*
* @param string $langcode
* The language code.
* @param string $context
* The string context.
* @param \Drupal\locale\StringStorageInterface $string_storage
* The string storage.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct($langcode, $context, StringStorageInterface $string_storage, CacheBackendInterface $cache, LockBackendInterface $lock, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, RequestStack $request_stack) {
$this->langcode = $langcode;
$this->context = (string) $context;
$this->stringStorage = $string_storage;
$this->configFactory = $config_factory;
$this->languageManager = $language_manager;
$this->cache = $cache;
$this->lock = $lock;
$this->tags = array('locale');
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
protected function getCid() {
if (!isset($this->cid)) {
// Add the current user's role IDs to the cache key, this ensures that,
// for example, strings for admin menu items and settings forms are not
// cached for anonymous users.
$user = \Drupal::currentUser();
$rids = $user ? implode(':', array_keys($user->getRoles())) : '0';
$this->cid = "locale:{$this->langcode}:{$this->context}:$rids";
// Getting the roles from the current user might have resulted in t()
// calls that attempted to get translations from the locale cache. In that
// case they would not go into this method again as
// CacheCollector::lazyLoadCache() already set the loaded flag. They would
// however call resolveCacheMiss() and add that string to the list of
// cache misses that need to be written into the cache. Prevent that by
// resetting that list. All that happens in such a case are a few uncached
// translation lookups.
$this->keysToPersist = array();
}
return $this->cid;
}
/**
* {@inheritdoc}
*/
protected function resolveCacheMiss($offset) {
$translation = $this->stringStorage->findTranslation(array(
'language' => $this->langcode,
'source' => $offset,
'context' => $this->context,
));
if ($translation) {
$value = !empty($translation->translation) ? $translation->translation : TRUE;
}
else {
// We don't have the source string, update the {locales_source} table to
// indicate the string is not translated.
$this->stringStorage->createString(array(
'source' => $offset,
'context' => $this->context,
'version' => \Drupal::VERSION,
))->addLocation('path', $this->requestStack->getCurrentRequest()->getRequestUri())->save();
$value = TRUE;
}
// If there is no translation available for the current language then use
// language fallback to try other translations.
if ($value === TRUE) {
$fallbacks = $this->languageManager->getFallbackCandidates(array('langcode' => $this->langcode, 'operation' => 'locale_lookup', 'data' => $offset));
if (!empty($fallbacks)) {
foreach ($fallbacks as $langcode) {
$translation = $this->stringStorage->findTranslation(array(
'language' => $langcode,
'source' => $offset,
'context' => $this->context,
));
if ($translation && !empty($translation->translation)) {
$value = $translation->translation;
break;
}
}
}
}
$this->storage[$offset] = $value;
// Disabling the usage of string caching allows a module to watch for
// the exact list of strings used on a page. From a performance
// perspective that is a really bad idea, so we have no user
// interface for this. Be careful when turning this option off!
if ($this->configFactory->get('locale.settings')->get('cache_strings')) {
$this->persist($offset);
}
return $value;
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleProjectStorage.
*/
namespace Drupal\locale;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
/**
* Provides the locale project storage system using a key value store.
*/
class LocaleProjectStorage implements LocaleProjectStorageInterface {
/**
* The key value store to use.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValueStore;
/**
* Static state cache.
*
* @var array
*/
protected $cache = array();
/**
* Cache status flag.
*
* @var bool
*/
protected static $all = FALSE;
/**
* Constructs a State object.
*
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The key value store to use.
*/
function __construct(KeyValueFactoryInterface $key_value_factory) {
$this->keyValueStore = $key_value_factory->get('locale.project');
}
/**
* {@inheritdoc}
*/
public function get($key, $default = NULL) {
$values = $this->getMultiple(array($key));
return isset($values[$key]) ? $values[$key] : $default;
}
/**
* {@inheritdoc}
*/
public function getMultiple(array $keys) {
$values = array();
$load = array();
foreach ($keys as $key) {
// Check if we have a value in the cache.
if (isset($this->cache[$key])) {
$values[$key] = $this->cache[$key];
}
// Load the value if we don't have an explicit NULL value.
elseif (!array_key_exists($key, $this->cache)) {
$load[] = $key;
}
}
if ($load) {
$loaded_values = $this->keyValueStore->getMultiple($load);
foreach ($load as $key) {
// If we find a value, even one that is NULL, add it to the cache and
// return it.
if (isset($loaded_values[$key])) {
$values[$key] = $loaded_values[$key];
$this->cache[$key] = $loaded_values[$key];
}
else {
$this->cache[$key] = NULL;
}
}
}
return $values;
}
/**
* {@inheritdoc}
*/
public function set($key, $value) {
$this->setMultiple(array($key => $value));
}
/**
* {@inheritdoc}
*/
public function setMultiple(array $data) {
foreach ($data as $key => $value) {
$this->cache[$key] = $value;
}
$this->keyValueStore->setMultiple($data);
}
/**
* {@inheritdoc}
*/
public function delete($key) {
$this->deleteMultiple(array($key));
}
/**
* {@inheritdoc}
*/
public function deleteMultiple(array $keys) {
foreach ($keys as $key) {
$this->cache[$key] = NULL;
}
$this->keyValueStore->deleteMultiple($keys);
}
/**
* {@inheritdoc}
*/
public function resetCache() {
$this->cache = array();
static::$all = FALSE;
}
/**
* {@inheritdoc}
*/
public function deleteAll() {
$this->keyValueStore->deleteAll();
$this->resetCache();
}
/**
* {@inheritdoc}
*/
public function disableAll() {
$projects = $this->keyValueStore->getAll();
foreach (array_keys($projects) as $key) {
$projects[$key]['status'] = 0;
if (isset($cache[$key])) {
$cache[$key] = $projects[$key];
}
}
$this->keyValueStore->setMultiple($projects);
}
/**
* {@inheritdoc}
*/
public function countProjects() {
return count($this->getAll());
}
/**
* {@inheritdoc}
*/
public function getAll() {
if (!static::$all) {
$this->cache = $this->keyValueStore->getAll();
static::$all = TRUE;
}
return $this->cache;
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleProjectStorageInterface.
*/
namespace Drupal\locale;
/**
* Defines the locale project storage interface.
*/
interface LocaleProjectStorageInterface {
/**
* Returns the stored value for a given key.
*
* @param string $key
* The key of the data to retrieve.
* @param mixed $default
* The default value to use if the key is not found.
*
* @return mixed
* The stored value, or the default value if no value exists.
*/
public function get($key, $default = NULL);
/**
* Returns a list of project records.
*
* @param array $keys
* A list of keys to retrieve.
*
* @return array
* An associative array of items successfully returned, indexed by key.
*/
public function getMultiple(array $keys);
/**
* Creates or updates the project record.
*
* @param string $key
* The key of the data to store.
* @param mixed $value
* The data to store.
*/
public function set($key, $value);
/**
* Creates or updates multiple project records.
*
* @param array $data
* An associative array of key/value pairs.
*/
public function setMultiple(array $data);
/**
* Deletes project records for a given key.
*
* @param string $key
* The key of the data to delete.
*/
public function delete($key);
/**
* Deletes multiple project records.
*
* @param array $keys
* A list of item names to delete.
*/
public function deleteMultiple(array $keys);
/**
* Returns all the project records.
*
* @return array
* An associative array of items successfully returned, indexed by key.
*/
public function getAll();
/**
* Deletes all projects records.
*
* @return array
* An associative array of items successfully returned, indexed by key.
*/
public function deleteAll();
/**
* Mark all projects as disabled.
*/
public function disableAll();
/**
* Resets the project storage cache.
*/
public function resetCache();
/**
* Returns the count of project records.
*
* @return int
* The number of saved items.
*/
public function countProjects();
}

View file

@ -0,0 +1,161 @@
<?php
/**
* @file
* Contains \Drupal\locale\LocaleTranslation.
*/
namespace Drupal\locale;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\StringTranslation\Translator\TranslatorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* String translator using the locale module.
*
* Full featured translation system using locale's string storage and
* database caching.
*/
class LocaleTranslation implements TranslatorInterface, DestructableInterface {
/**
* Storage for strings.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected $storage;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Cached translations.
*
* @var array
* Array of \Drupal\locale\LocaleLookup objects indexed by language code
* and context.
*/
protected $translations = array();
/**
* The cache backend that should be used.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The lock backend that should be used.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* The translate english configuration value.
*
* @var bool
*/
protected $translateEnglish;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a translator using a string storage.
*
* @param \Drupal\locale\StringStorageInterface $storage
* Storage to use when looking for new translations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(StringStorageInterface $storage, CacheBackendInterface $cache, LockBackendInterface $lock, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, RequestStack $request_stack) {
$this->storage = $storage;
$this->cache = $cache;
$this->lock = $lock;
$this->configFactory = $config_factory;
$this->languageManager = $language_manager;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public function getStringTranslation($langcode, $string, $context) {
// If the language is not suitable for locale module, just return.
if ($langcode == LanguageInterface::LANGCODE_SYSTEM || ($langcode == 'en' && !$this->canTranslateEnglish())) {
return FALSE;
}
// Strings are cached by langcode, context and roles, using instances of the
// LocaleLookup class to handle string lookup and caching.
if (!isset($this->translations[$langcode][$context])) {
$this->translations[$langcode][$context] = new LocaleLookup($langcode, $context, $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack);
}
$translation = $this->translations[$langcode][$context]->get($string);
return $translation === TRUE ? FALSE : $translation;
}
/**
* Gets translate english configuration value.
*
* @return bool
* TRUE if english should be translated, FALSE if not.
*/
protected function canTranslateEnglish() {
if (!isset($this->translateEnglish)) {
$this->translateEnglish = $this->configFactory->get('locale.settings')->get('translate_english');
}
return $this->translateEnglish;
}
/**
* {@inheritdoc}
*/
public function reset() {
unset($this->translateEnglish);
$this->translations = array();
}
/**
* {@inheritdoc}
*/
public function destruct() {
foreach ($this->translations as $context) {
foreach ($context as $lookup) {
if ($lookup instanceof DestructableInterface) {
$lookup->destruct();
}
}
}
}
}

View file

@ -0,0 +1,122 @@
<?php
/**
* @file
* Contains \Drupal\locale\Plugin\QueueWorker\LocaleTranslation.
*/
namespace Drupal\locale\Plugin\QueueWorker;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Executes interface translation queue tasks.
*
* @QueueWorker(
* id = "locale_translation",
* title = @Translation("Update translations"),
* cron = {"time" = 30}
* )
*/
class LocaleTranslation extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The queue object.
*
* @var \Drupal\Core\Queue\QueueInterface
*/
protected $queue;
/**
* Constructs a new LocaleTranslation object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Queue\QueueInterface $queue
* The queue object.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, ModuleHandlerInterface $module_handler, QueueInterface $queue) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->queue = $queue;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('queue')->get('locale_translation', TRUE)
);
}
/**
* {@inheritdoc}
*
* The translation update functions executed here are batch operations which
* are also used in translation update batches. The batch functions may need
* to be executed multiple times to complete their task, typically this is the
* translation import function. When a batch function is not finished, a new
* queue task is created and added to the end of the queue. The batch context
* data is needed to continue the batch task is stored in the queue with the
* queue data.
*/
public function processItem($data) {
$this->moduleHandler->loadInclude('locale', 'batch.inc');
list($function, $args) = $data;
// We execute batch operation functions here to check, download and import
// the translation files. Batch functions use a context variable as last
// argument which is passed by reference. When a batch operation is called
// for the first time a default batch context is created. When called
// iterative (usually the batch import function) the batch context is passed
// through via the queue and is part of the $data.
$last = count($args) - 1;
if (!is_array($args[$last]) || !isset($args[$last]['finished'])) {
$batch_context = [
'sandbox' => [],
'results' => [],
'finished' => 1,
'message' => '',
];
}
else {
$batch_context = $args[$last];
unset ($args[$last]);
}
$args = array_merge($args, [&$batch_context]);
// Call the batch operation function.
call_user_func_array($function, $args);
// If the batch operation is not finished we create a new queue task to
// continue the task. This is typically the translation import task.
if ($batch_context['finished'] < 1) {
unset($batch_context['strings']);
$this->queue->createItem([$function, $args]);
}
}
}

View file

@ -0,0 +1,177 @@
<?php
/**
* @file
* Contains \Drupal\locale\PoDatabaseReader.
*/
namespace Drupal\locale;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoReaderInterface;
use Drupal\locale\TranslationString;
/**
* Gettext PO reader working with the locale module database.
*/
class PoDatabaseReader implements PoReaderInterface {
/**
* An associative array indicating which type of strings should be read.
*
* Elements of the array:
* - not_customized: boolean indicating if not customized strings should be
* read.
* - customized: boolean indicating if customized strings should be read.
* - no_translated: boolean indicating if non-translated should be read.
*
* The three options define three distinct sets of strings, which combined
* cover all strings.
*
* @var array
*/
private $options;
/**
* Language code of the language being read from the database.
*
* @var string
*/
private $langcode;
/**
* Store the result of the query so it can be iterated later.
*
* @var resource
*/
private $result;
/**
* Constructor, initializes with default options.
*/
public function __construct() {
$this->setOptions(array());
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->langcode;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->langcode = $langcode;
}
/**
* Get the options used by the reader.
*/
public function getOptions() {
return $this->options;
}
/**
* Set the options for the current reader.
*/
public function setOptions(array $options) {
$options += array(
'customized' => FALSE,
'not_customized' => FALSE,
'not_translated' => FALSE,
);
$this->options = $options;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader().
*/
public function getHeader() {
return new PoHeader($this->getLangcode());
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader().
*
* @throws Exception
* Always, because you cannot set the PO header of a reader.
*/
public function setHeader(PoHeader $header) {
throw new \Exception('You cannot set the PO header in a reader.');
}
/**
* Builds and executes a database query based on options set earlier.
*/
private function loadStrings() {
$langcode = $this->langcode;
$options = $this->options;
$conditions = array();
if (array_sum($options) == 0) {
// If user asked to not include anything in the translation files,
// that would not make sense, so just fall back on providing a template.
$langcode = NULL;
// Force option to get both translated and untranslated strings.
$options['not_translated'] = TRUE;
}
// Build and execute query to collect source strings and translations.
if (!empty($langcode)) {
$conditions['language'] = $langcode;
// Translate some options into field conditions.
if ($options['customized']) {
if (!$options['not_customized']) {
// Filter for customized strings only.
$conditions['customized'] = LOCALE_CUSTOMIZED;
}
// Else no filtering needed in this case.
}
else {
if ($options['not_customized']) {
// Filter for non-customized strings only.
$conditions['customized'] = LOCALE_NOT_CUSTOMIZED;
}
else {
// Filter for strings without translation.
$conditions['translated'] = FALSE;
}
}
if (!$options['not_translated']) {
// Filter for string with translation.
$conditions['translated'] = TRUE;
}
return \Drupal::service('locale.storage')->getTranslations($conditions);
}
else {
// If no language, we don't need any of the target fields.
return \Drupal::service('locale.storage')->getStrings($conditions);
}
}
/**
* Get the database result resource for the given language and options.
*/
private function readString() {
if (!isset($this->result)) {
$this->result = $this->loadStrings();
}
return array_shift($this->result);
}
/**
* Implements Drupal\Component\Gettext\PoReaderInterface::readItem().
*/
public function readItem() {
if ($string = $this->readString()) {
$values = (array) $string;
$po_item = new PoItem();
$po_item->setFromArray($values);
return $po_item;
}
}
}

View file

@ -0,0 +1,290 @@
<?php
/**
* @file
* Contains \Drupal\locale\PoDatabaseWriter.
*/
namespace Drupal\locale;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoReaderInterface;
use Drupal\Component\Gettext\PoWriterInterface;
use Drupal\locale\SourceString;
use Drupal\locale\TranslationString;
/**
* Gettext PO writer working with the locale module database.
*/
class PoDatabaseWriter implements PoWriterInterface {
/**
* An associative array indicating what data should be overwritten, if any.
*
* Elements of the array:
* - override_options
* - not_customized: boolean indicating that not customized strings should
* be overwritten.
* - customized: boolean indicating that customized strings should be
* overwritten.
* - customized: the strings being imported should be saved as customized.
* One of LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED.
*
* @var array
*/
private $options;
/**
* Language code of the language being written to the database.
*
* @var string
*/
private $langcode;
/**
* Header of the po file written to the database.
*
* @var \Drupal\Component\Gettext\PoHeader
*/
private $header;
/**
* Associative array summarizing the number of changes done.
*
* Keys for the array:
* - additions: number of source strings newly added
* - updates: number of translations updated
* - deletes: number of translations deleted
* - skips: number of strings skipped due to disallowed HTML
*
* @var array
*/
private $report;
/**
* Constructor, initialize reporting array.
*/
public function __construct() {
$this->setReport();
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->langcode;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->langcode = $langcode;
}
/**
* Get the report of the write operations.
*/
public function getReport() {
return $this->report;
}
/**
* Set the report array of write operations.
*
* @param array $report
* Associative array with result information.
*/
public function setReport($report = array()) {
$report += array(
'additions' => 0,
'updates' => 0,
'deletes' => 0,
'skips' => 0,
'strings' => array(),
);
$this->report = $report;
}
/**
* Get the options used by the writer.
*/
public function getOptions() {
return $this->options;
}
/**
* Set the options for the current writer.
*/
public function setOptions(array $options) {
if (!isset($options['overwrite_options'])) {
$options['overwrite_options'] = array();
}
$options['overwrite_options'] += array(
'not_customized' => FALSE,
'customized' => FALSE,
);
$options += array(
'customized' => LOCALE_NOT_CUSTOMIZED,
);
$this->options = $options;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader().
*/
public function getHeader() {
return $this->header;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader().
*
* Sets the header and configure Drupal accordingly.
*
* Before being able to process the given header we need to know in what
* context this database write is done. For this the options must be set.
*
* A langcode is required to set the current header's PluralForm.
*
* @param \Drupal\Component\Gettext\PoHeader $header
* Header metadata.
*
* @throws Exception
*/
public function setHeader(PoHeader $header) {
$this->header = $header;
$locale_plurals = \Drupal::state()->get('locale.translation.plurals') ?: array();
// Check for options.
$options = $this->getOptions();
if (empty($options)) {
throw new \Exception('Options should be set before assigning a PoHeader.');
}
$overwrite_options = $options['overwrite_options'];
// Check for langcode.
$langcode = $this->langcode;
if (empty($langcode)) {
throw new \Exception('Langcode should be set before assigning a PoHeader.');
}
if (array_sum($overwrite_options) || empty($locale_plurals[$langcode]['plurals'])) {
// Get and store the plural formula if available.
$plural = $header->getPluralForms();
if (isset($plural) && $p = $header->parsePluralForms($plural)) {
list($nplurals, $formula) = $p;
$locale_plurals[$langcode] = array(
'plurals' => $nplurals,
'formula' => $formula,
);
\Drupal::state()->set('locale.translation.plurals', $locale_plurals);
}
}
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItem().
*/
public function writeItem(PoItem $item) {
if ($item->isPlural()) {
$item->setSource(implode(LOCALE_PLURAL_DELIMITER, $item->getSource()));
$item->setTranslation(implode(LOCALE_PLURAL_DELIMITER, $item->getTranslation()));
}
$this->importString($item);
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Imports one string into the database.
*
* @param \Drupal\Component\Gettext\PoItem $item
* The item being imported.
*
* @return int
* The string ID of the existing string modified or the new string added.
*/
private function importString(PoItem $item) {
// Initialize overwrite options if not set.
$this->options['overwrite_options'] += array(
'not_customized' => FALSE,
'customized' => FALSE,
);
$overwrite_options = $this->options['overwrite_options'];
$customized = $this->options['customized'];
$context = $item->getContext();
$source = $item->getSource();
$translation = $item->getTranslation();
// Look up the source string and any existing translation.
$strings = \Drupal::service('locale.storage')->getTranslations(array(
'language' => $this->langcode,
'source' => $source,
'context' => $context,
));
$string = reset($strings);
if (!empty($translation)) {
// Skip this string unless it passes a check for dangerous code.
if (!locale_string_is_safe($translation)) {
\Drupal::logger('locale')->error('Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $translation));
$this->report['skips']++;
return 0;
}
elseif ($string) {
$string->setString($translation);
if ($string->isNew()) {
// No translation in this language.
$string->setValues(array(
'language' => $this->langcode,
'customized' => $customized,
));
$string->save();
$this->report['additions']++;
}
elseif ($overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
// Translation exists, only overwrite if instructed.
$string->customized = $customized;
$string->save();
$this->report['updates']++;
}
$this->report['strings'][] = $string->getId();
return $string->lid;
}
else {
// No such source string in the database yet.
$string = \Drupal::service('locale.storage')->createString(array('source' => $source, 'context' => $context))
->save();
\Drupal::service('locale.storage')->createTranslation(array(
'lid' => $string->getId(),
'language' => $this->langcode,
'translation' => $translation,
'customized' => $customized,
))->save();
$this->report['additions']++;
$this->report['strings'][] = $string->getId();
return $string->lid;
}
}
elseif ($string && !$string->isNew() && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
// Empty translation, remove existing if instructed.
$string->delete();
$this->report['deletes']++;
$this->report['strings'][] = $string->lid;
return $string->lid;
}
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\locale\SourceString.
*/
namespace Drupal\locale;
use Drupal\locale\LocaleString;
/**
* Defines the locale source string object.
*
* This class represents a module-defined string value that is to be translated.
* This string must at least contain a 'source' field, which is the raw source
* value, and is assumed to be in English language.
*/
class SourceString extends StringBase {
/**
* Implements Drupal\locale\StringInterface::isSource().
*/
public function isSource() {
return isset($this->source);
}
/**
* Implements Drupal\locale\StringInterface::isTranslation().
*/
public function isTranslation() {
return FALSE;
}
/**
* Implements Drupal\locale\LocaleString::getString().
*/
public function getString() {
return isset($this->source) ? $this->source : '';
}
/**
* Implements Drupal\locale\LocaleString::setString().
*/
public function setString($string) {
$this->source = $string;
return $this;
}
/**
* Implements Drupal\locale\LocaleString::isNew().
*/
public function isNew() {
return empty($this->lid);
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\locale\StreamWrapper\TranslationsStream.
*/
namespace Drupal\locale\StreamWrapper;
use Drupal\Core\Annotation\StreamWrapper;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
/**
* Defines a Drupal translations (translations://) stream wrapper class.
*
* Provides support for storing translation files.
*/
class TranslationsStream extends LocalStream {
/**
* {@inheritdoc}
*/
public static function getType() {
return StreamWrapperInterface::LOCAL_HIDDEN;
}
/**
* {@inheritdoc}
*/
public function getName() {
return t('Translation files');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return t('Translation files');
}
/**
* Implements Drupal\Core\StreamWrapper\LocalStream::getDirectoryPath()
*/
function getDirectoryPath() {
return \Drupal::config('locale.settings')->get('translation.path');
}
/**
* Implements Drupal\Core\StreamWrapper\StreamWrapperInterface::getExternalUrl().
* @throws \LogicException PO files URL should not be public.
*/
function getExternalUrl() {
throw new \LogicException('PO files URL should not be public.');
}
}

View file

@ -0,0 +1,217 @@
<?php
/**
* @file
* Contains \Drupal\locale\StringBase.
*/
namespace Drupal\locale;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines the locale string base class.
*
* This is the base class to be used for locale string objects and contains
* the common properties and methods for source and translation strings.
*/
abstract class StringBase implements StringInterface {
/**
* The string identifier.
*
* @var integer
*/
public $lid;
/**
* The string locations indexed by type.
*
* @var string
*/
public $locations;
/**
* The source string.
*
* @var string
*/
public $source;
/**
* The string context.
*
* @var string
*/
public $context;
/**
* The string version.
*
* @var string
*/
public $version;
/**
* The locale storage this string comes from or is to be saved to.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected $storage;
/**
* Constructs a new locale string object.
*
* @param object|array $values
* Object or array with initial values.
*/
public function __construct($values = array()) {
$this->setValues((array) $values);
}
/**
* Implements Drupal\locale\StringInterface::getId().
*/
public function getId() {
return isset($this->lid) ? $this->lid : NULL;
}
/**
* Implements Drupal\locale\StringInterface::setId().
*/
public function setId($lid) {
$this->lid = $lid;
return $this;
}
/**
* Implements Drupal\locale\StringInterface::getVersion().
*/
public function getVersion() {
return isset($this->version) ? $this->version : NULL;
}
/**
* Implements Drupal\locale\StringInterface::setVersion().
*/
public function setVersion($version) {
$this->version = $version;
return $this;
}
/**
* Implements Drupal\locale\StringInterface::getPlurals().
*/
public function getPlurals() {
return explode(LOCALE_PLURAL_DELIMITER, $this->getString());
}
/**
* Implements Drupal\locale\StringInterface::setPlurals().
*/
public function setPlurals($plurals) {
$this->setString(implode(LOCALE_PLURAL_DELIMITER, $plurals));
return $this;
}
/**
* Implements Drupal\locale\StringInterface::getStorage().
*/
public function getStorage() {
return isset($this->storage) ? $this->storage : NULL;
}
/**
* Implements Drupal\locale\StringInterface::setStorage().
*/
public function setStorage($storage) {
$this->storage = $storage;
return $this;
}
/**
* Implements Drupal\locale\StringInterface::setValues().
*/
public function setValues(array $values, $override = TRUE) {
foreach ($values as $key => $value) {
if (property_exists($this, $key) && ($override || !isset($this->$key))) {
$this->$key = $value;
}
}
return $this;
}
/**
* Implements Drupal\locale\StringInterface::getValues().
*/
public function getValues(array $fields) {
$values = array();
foreach ($fields as $field) {
if (isset($this->$field)) {
$values[$field] = $this->$field;
}
}
return $values;
}
/**
* Implements Drupal\locale\StringInterface::getLocation().
*/
public function getLocations($check_only = FALSE) {
if (!isset($this->locations) && !$check_only) {
$this->locations = array();
foreach ($this->getStorage()->getLocations(array('sid' => $this->getId())) as $location) {
$this->locations[$location->type][$location->name] = $location->lid;
}
}
return isset($this->locations) ? $this->locations : array();
}
/**
* Implements Drupal\locale\StringInterface::addLocation().
*/
public function addLocation($type, $name) {
$this->locations[$type][$name] = TRUE;
return $this;
}
/**
* Implements Drupal\locale\StringInterface::hasLocation().
*/
public function hasLocation($type, $name) {
$locations = $this->getLocations();
return isset($locations[$type]) ? !empty($locations[$type][$name]) : FALSE;
}
/**
* Implements Drupal\locale\LocaleString::save().
*/
public function save() {
if ($storage = $this->getStorage()) {
$storage->save($this);
}
else {
throw new StringStorageException(SafeMarkup::format('The string cannot be saved because its not bound to a storage: @string', array(
'@string' => $this->getString(),
)));
}
return $this;
}
/**
* Implements Drupal\locale\LocaleString::delete().
*/
public function delete() {
if (!$this->isNew()) {
if ($storage = $this->getStorage()) {
$storage->delete($this);
}
else {
throw new StringStorageException(SafeMarkup::format('The string cannot be deleted because its not bound to a storage: @string', array(
'@string' => $this->getString(),
)));
}
}
return $this;
}
}

View file

@ -0,0 +1,550 @@
<?php
/**
* @file
* Contains \Drupal\locale\StringDatabaseStorage.
*/
namespace Drupal\locale;
use Drupal\Core\Database\Connection;
/**
* Defines a class to store localized strings in the database.
*/
class StringDatabaseStorage implements StringStorageInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Additional database connection options to use in queries.
*
* @var array
*/
protected $options = array();
/**
* Constructs a new StringDatabaseStorage class.
*
* @param \Drupal\Core\Database\Connection $connection
* A Database connection to use for reading and writing configuration data.
* @param array $options
* (optional) Any additional database connection options to use in queries.
*/
public function __construct(Connection $connection, array $options = array()) {
$this->connection = $connection;
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public function getStrings(array $conditions = array(), array $options = array()) {
return $this->dbStringLoad($conditions, $options, 'Drupal\locale\SourceString');
}
/**
* {@inheritdoc}
*/
public function getTranslations(array $conditions = array(), array $options = array()) {
return $this->dbStringLoad($conditions, array('translation' => TRUE) + $options, 'Drupal\locale\TranslationString');
}
/**
* {@inheritdoc}
*/
public function findString(array $conditions) {
$values = $this->dbStringSelect($conditions)
->execute()
->fetchAssoc();
if (!empty($values)) {
$string = new SourceString($values);
$string->setStorage($this);
return $string;
}
}
/**
* {@inheritdoc}
*/
public function findTranslation(array $conditions) {
$values = $this->dbStringSelect($conditions, array('translation' => TRUE))
->execute()
->fetchAssoc();
if (!empty($values)) {
$string = new TranslationString($values);
$this->checkVersion($string, \Drupal::VERSION);
$string->setStorage($this);
return $string;
}
}
/**
* {@inheritdoc}
*/
public function getLocations(array $conditions = array()) {
$query = $this->connection->select('locales_location', 'l', $this->options)
->fields('l');
foreach ($conditions as $field => $value) {
// Cast scalars to array so we can consistently use an IN condition.
$query->condition('l.' . $field, (array) $value, 'IN');
}
return $query->execute()->fetchAll();
}
/**
* {@inheritdoc}
*/
public function countStrings() {
return $this->dbExecute("SELECT COUNT(*) FROM {locales_source}")->fetchField();
}
/**
* {@inheritdoc}
*/
public function countTranslations() {
return $this->dbExecute("SELECT t.language, COUNT(*) AS translated FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY t.language")->fetchAllKeyed();
}
/**
* {@inheritdoc}
*/
public function save($string) {
if ($string->isNew()) {
$result = $this->dbStringInsert($string);
if ($string->isSource() && $result) {
// Only for source strings, we set the locale identifier.
$string->setId($result);
}
$string->setStorage($this);
}
else {
$this->dbStringUpdate($string);
}
// Update locations if they come with the string.
$this->updateLocation($string);
return $this;
}
/**
* Update locations for string.
*
* @param \Drupal\locale\StringInterface $string
* The string object.
*/
protected function updateLocation($string) {
if ($locations = $string->getLocations(TRUE)) {
$created = FALSE;
foreach ($locations as $type => $location) {
foreach ($location as $name => $lid) {
// Make sure that the name isn't longer than 255 characters.
$name = substr($name, 0, 255);
if (!$lid) {
$this->dbDelete('locales_location', array('sid' => $string->getId(), 'type' => $type, 'name' => $name))
->execute();
}
elseif ($lid === TRUE) {
// This is a new location to add, take care not to duplicate.
$this->connection->merge('locales_location', $this->options)
->keys(array('sid' => $string->getId(), 'type' => $type, 'name' => $name))
->fields(array('version' => \Drupal::VERSION))
->execute();
$created = TRUE;
}
// Loaded locations have 'lid' integer value, nor FALSE, nor TRUE.
}
}
if ($created) {
// As we've set a new location, check string version too.
$this->checkVersion($string, \Drupal::VERSION);
}
}
}
/**
* Checks whether the string version matches a given version, fix it if not.
*
* @param \Drupal\locale\StringInterface $string
* The string object.
* @param string $version
* Drupal version to check against.
*/
protected function checkVersion($string, $version) {
if ($string->getId() && $string->getVersion() != $version) {
$string->setVersion($version);
$this->connection->update('locales_source', $this->options)
->condition('lid', $string->getId())
->fields(array('version' => $version))
->execute();
}
}
/**
* {@inheritdoc}
*/
public function delete($string) {
if ($keys = $this->dbStringKeys($string)) {
$this->dbDelete('locales_target', $keys)->execute();
if ($string->isSource()) {
$this->dbDelete('locales_source', $keys)->execute();
$this->dbDelete('locales_location', $keys)->execute();
$string->setId(NULL);
}
}
else {
throw new StringStorageException(format_string('The string cannot be deleted because it lacks some key fields: @string', array(
'@string' => $string->getString(),
)));
}
return $this;
}
/**
* {@inheritdoc}
*/
public function deleteStrings($conditions) {
$lids = $this->dbStringSelect($conditions, array('fields' => array('lid')))->execute()->fetchCol();
if ($lids) {
$this->dbDelete('locales_target', array('lid' => $lids))->execute();
$this->dbDelete('locales_source', array('lid' => $lids))->execute();
$this->dbDelete('locales_location', array('sid' => $lids))->execute();
}
}
/**
* {@inheritdoc}
*/
public function deleteTranslations($conditions) {
$this->dbDelete('locales_target', $conditions)->execute();
}
/**
* {@inheritdoc}
*/
public function createString($values = array()) {
return new SourceString($values + array('storage' => $this));
}
/**
* {@inheritdoc}
*/
public function createTranslation($values = array()) {
return new TranslationString($values + array(
'storage' => $this,
'is_new' => TRUE,
));
}
/**
* Gets table alias for field.
*
* @param string $field
* One of the field names of the locales_source, locates_location,
* locales_target tables to find the table alias for.
*
* @return string
* One of the following values:
* - 's' for "source", "context", "version" (locales_source table fields).
* - 'l' for "type", "name" (locales_location table fields)
* - 't' for "language", "translation", "customized" (locales_target
* table fields)
*/
protected function dbFieldTable($field) {
if (in_array($field, array('language', 'translation', 'customized'))) {
return 't';
}
elseif (in_array($field, array('type', 'name'))) {
return 'l';
}
else {
return 's';
}
}
/**
* Gets table name for storing string object.
*
* @param \Drupal\locale\StringInterface $string
* The string object.
*
* @return string
* The table name.
*/
protected function dbStringTable($string) {
if ($string->isSource()) {
return 'locales_source';
}
elseif ($string->isTranslation()) {
return 'locales_target';
}
}
/**
* Gets keys values that are in a database table.
*
* @param \Drupal\locale\StringInterface $string
* The string object.
*
* @return array
* Array with key fields if the string has all keys, or empty array if not.
*/
protected function dbStringKeys($string) {
if ($string->isSource()) {
$keys = array('lid');
}
elseif ($string->isTranslation()) {
$keys = array('lid', 'language');
}
if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
return $values;
}
else {
return array();
}
}
/**
* Loads multiple string objects.
*
* @param array $conditions
* Any of the conditions used by dbStringSelect().
* @param array $options
* Any of the options used by dbStringSelect().
* @param string $class
* Class name to use for fetching returned objects.
*
* @return \Drupal\locale\StringInterface[]
* Array of objects of the class requested.
*/
protected function dbStringLoad(array $conditions, array $options, $class) {
$strings = array();
$result = $this->dbStringSelect($conditions, $options)->execute();
foreach ($result as $item) {
/** @var \Drupal\locale\StringInterface $string */
$string = new $class($item);
$string->setStorage($this);
$strings[] = $string;
}
return $strings;
}
/**
* Builds a SELECT query with multiple conditions and fields.
*
* The query uses both 'locales_source' and 'locales_target' tables.
* Note that by default, as we are selecting both translated and untranslated
* strings target field's conditions will be modified to match NULL rows too.
*
* @param array $conditions
* An associative array with field => value conditions that may include
* NULL values. If a language condition is included it will be used for
* joining the 'locales_target' table.
* @param array $options
* An associative array of additional options. It may contain any of the
* options used by Drupal\locale\StringStorageInterface::getStrings() and
* these additional ones:
* - 'translation', Whether to include translation fields too. Defaults to
* FALSE.
*
* @return \Drupal\Core\Database\Query\Select
* Query object with all the tables, fields and conditions.
*/
protected function dbStringSelect(array $conditions, array $options = array()) {
// Start building the query with source table and check whether we need to
// join the target table too.
$query = $this->connection->select('locales_source', 's', $this->options)
->fields('s');
// Figure out how to join and translate some options into conditions.
if (isset($conditions['translated'])) {
// This is a meta-condition we need to translate into simple ones.
if ($conditions['translated']) {
// Select only translated strings.
$join = 'innerJoin';
}
else {
// Select only untranslated strings.
$join = 'leftJoin';
$conditions['translation'] = NULL;
}
unset($conditions['translated']);
}
else {
$join = !empty($options['translation']) ? 'leftJoin' : FALSE;
}
if ($join) {
if (isset($conditions['language'])) {
// If we've got a language condition, we use it for the join.
$query->$join('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array(
':langcode' => $conditions['language'],
));
unset($conditions['language']);
}
else {
// Since we don't have a language, join with locale id only.
$query->$join('locales_target', 't', "t.lid = s.lid");
}
if (!empty($options['translation'])) {
// We cannot just add all fields because 'lid' may get null values.
$query->fields('t', array('language', 'translation', 'customized'));
}
}
// If we have conditions for location's type or name, then we need the
// location table, for which we add a subquery. We cast any scalar value to
// array so we can consistently use IN conditions.
if (isset($conditions['type']) || isset($conditions['name'])) {
$subquery = $this->connection->select('locales_location', 'l', $this->options)
->fields('l', array('sid'));
foreach (array('type', 'name') as $field) {
if (isset($conditions[$field])) {
$subquery->condition('l.' . $field, (array) $conditions[$field], 'IN');
unset($conditions[$field]);
}
}
$query->condition('s.lid', $subquery, 'IN');
}
// Add conditions for both tables.
foreach ($conditions as $field => $value) {
$table_alias = $this->dbFieldTable($field);
$field_alias = $table_alias . '.' . $field;
if (is_null($value)) {
$query->isNull($field_alias);
}
elseif ($table_alias == 't' && $join === 'leftJoin') {
// Conditions for target fields when doing an outer join only make
// sense if we add also OR field IS NULL.
$query->condition(db_or()
->condition($field_alias, (array) $value, 'IN')
->isNull($field_alias)
);
}
else {
$query->condition($field_alias, (array) $value, 'IN');
}
}
// Process other options, string filter, query limit, etc.
if (!empty($options['filters'])) {
if (count($options['filters']) > 1) {
$filter = db_or();
$query->condition($filter);
}
else {
// If we have a single filter, just add it to the query.
$filter = $query;
}
foreach ($options['filters'] as $field => $string) {
$filter->condition($this->dbFieldTable($field) . '.' . $field, '%' . db_like($string) . '%', 'LIKE');
}
}
if (!empty($options['pager limit'])) {
$query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit($options['pager limit']);
}
return $query;
}
/**
* Creates a database record for a string object.
*
* @param \Drupal\locale\StringInterface $string
* The string object.
*
* @return bool|int
* If the operation failed, returns FALSE.
* If it succeeded returns the last insert ID of the query, if one exists.
*
* @throws \Drupal\locale\StringStorageException
* If the string is not suitable for this storage, an exception is thrown.
*/
protected function dbStringInsert($string) {
if ($string->isSource()) {
$string->setValues(array('context' => '', 'version' => 'none'), FALSE);
$fields = $string->getValues(array('source', 'context', 'version'));
}
elseif ($string->isTranslation()) {
$string->setValues(array('customized' => 0), FALSE);
$fields = $string->getValues(array('lid', 'language', 'translation', 'customized'));
}
if (!empty($fields)) {
return $this->connection->insert($this->dbStringTable($string), $this->options)
->fields($fields)
->execute();
}
else {
throw new StringStorageException(format_string('The string cannot be saved: @string', array(
'@string' => $string->getString(),
)));
}
}
/**
* Updates string object in the database.
*
* @param \Drupal\locale\StringInterface $string
* The string object.
*
* @return bool|int
* If the record update failed, returns FALSE. If it succeeded, returns
* SAVED_NEW or SAVED_UPDATED.
*
* @throws \Drupal\locale\StringStorageException
* If the string is not suitable for this storage, an exception is thrown.
*/
protected function dbStringUpdate($string) {
if ($string->isSource()) {
$values = $string->getValues(array('source', 'context', 'version'));
}
elseif ($string->isTranslation()) {
$values = $string->getValues(array('translation', 'customized'));
}
if (!empty($values) && $keys = $this->dbStringKeys($string)) {
return $this->connection->merge($this->dbStringTable($string), $this->options)
->keys($keys)
->fields($values)
->execute();
}
else {
throw new StringStorageException(format_string('The string cannot be updated: @string', array(
'@string' => $string->getString(),
)));
}
}
/**
* Creates delete query.
*
* @param string $table
* The table name.
* @param array $keys
* Array with object keys indexed by field name.
*
* @return \Drupal\Core\Database\Query\Delete
* Returns a new Delete object for the injected database connection.
*/
protected function dbDelete($table, $keys) {
$query = $this->connection->delete($table, $this->options);
foreach ($keys as $field => $value) {
$query->condition($field, $value);
}
return $query;
}
/**
* Executes an arbitrary SELECT query string with the injected options.
*/
protected function dbExecute($query, array $args = array()) {
return $this->connection->query($query, $args, $this->options);
}
}

View file

@ -0,0 +1,221 @@
<?php
/**
* @file
* Contains \Drupal\locale\StringInterface.
*/
namespace Drupal\locale;
/**
* Defines the locale string interface.
*/
interface StringInterface {
/**
* Gets the string unique identifier.
*
* @return int
* The string identifier.
*/
public function getId();
/**
* Sets the string unique identifier.
*
* @param int $id
* The string identifier.
*
* @return $this
*/
public function setId($id);
/**
* Gets the string version.
*
* @return string
* Version identifier.
*/
public function getVersion();
/**
* Sets the string version.
*
* @param string $version
* Version identifier.
*
* @return $this
*/
public function setVersion($version);
/**
* Gets plain string contained in this object.
*
* @return string
* The string contained in this object.
*/
public function getString();
/**
* Sets the string contained in this object.
*
* @param string $string
* String to set as value.
*
* @return $this
*/
public function setString($string);
/**
* Splits string to work with plural values.
*
* @return array
* Array of strings that are plural variants.
*/
public function getPlurals();
/**
* Sets this string using array of plural values.
*
* Serializes plural variants in one string glued by LOCALE_PLURAL_DELIMITER.
*
* @param array $plurals
* Array of strings with plural variants.
*
* @return $this
*/
public function setPlurals($plurals);
/**
* Gets the string storage.
*
* @return \Drupal\locale\StringStorageInterface
* The storage used for this string.
*/
public function getStorage();
/**
* Sets the string storage.
*
* @param \Drupal\locale\StringStorageInterface $storage
* The storage to use for this string.
*
* @return $this
*/
public function setStorage($storage);
/**
* Checks whether the object is not saved to storage yet.
*
* @return bool
* TRUE if the object exists in the storage, FALSE otherwise.
*/
public function isNew();
/**
* Checks whether the object is a source string.
*
* @return bool
* TRUE if the object is a source string, FALSE otherwise.
*/
public function isSource();
/**
* Checks whether the object is a translation string.
*
* @return bool
* TRUE if the object is a translation string, FALSE otherwise.
*/
public function isTranslation();
/**
* Sets an array of values as object properties.
*
* @param array $values
* Array with values indexed by property name.
* @param bool $override
* (optional) Whether to override already set fields, defaults to TRUE.
*
* @return $this
*/
public function setValues(array $values, $override = TRUE);
/**
* Gets field values that are set for given field names.
*
* @param array $fields
* Array of field names.
*
* @return array
* Array of field values indexed by field name.
*/
public function getValues(array $fields);
/**
* Gets location information for this string.
*
* Locations are arbitrary pairs of type and name strings, used to store
* information about the origins of the string, like the file name it
* was found on, the path on which it was discovered, etc.
*
* A string can have any number of locations since the same string may be
* found on different places of Drupal code and configuration.
*
* @param bool $check_only
* (optional) Set to TRUE to get only new locations added during the
* current page request and not loading all existing locations.
*
* @return array
* Location ids indexed by type and name.
*/
public function getLocations($check_only = FALSE);
/**
* Adds a location for this string.
*
* @param string $type
* Location type that may be any arbitrary string. Types used in Drupal
* core are: 'javascript', 'path', 'code', 'configuration'.
* @param string $name
* Location name. Drupal path in case of online discovered translations,
* file path in case of imported strings, configuration name for strings
* that come from configuration, etc.
*
* @return $this
*/
public function addLocation($type, $name);
/**
* Checks whether the string has a given location.
*
* @param string $type
* Location type.
* @param string $name
* Location name.
*
* @return bool
* TRUE if the string has a location with this type and name.
*/
public function hasLocation($type, $name);
/**
* Saves string object to storage.
*
* @return $this
*
* @throws \Drupal\locale\StringStorageException
* In case of failures, an exception is thrown.
*/
public function save();
/**
* Deletes string object from storage.
*
* @return $this
*
* @throws \Drupal\locale\StringStorageException
* In case of failures, an exception is thrown.
*/
public function delete();
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\locale\StringStorageException.
*/
namespace Drupal\locale;
/**
* Defines an exception thrown when storage operations fail.
*/
class StringStorageException extends \Exception {}

View file

@ -0,0 +1,184 @@
<?php
/**
* @file
* Contains \Drupal\locale\StringStorageInterface.
*/
namespace Drupal\locale;
/**
* Defines the locale string storage interface.
*/
interface StringStorageInterface {
/**
* Loads multiple source string objects.
*
* @param array $conditions
* (optional) Array with conditions that will be used to filter the strings
* returned and may include any of the following elements:
* - Any simple field value indexed by field name.
* - 'translated', TRUE to get only translated strings or FALSE to get only
* untranslated strings. If not set it returns both translated and
* untranslated strings that fit the other conditions.
* Defaults to no conditions which means that it will load all strings.
* @param array $options
* (optional) An associative array of additional options. It may contain
* any of the following optional keys:
* - 'filters': Array of string filters indexed by field name.
* - 'pager limit': Use pager and set this limit value.
*
* @return array
* Array of \Drupal\locale\StringInterface objects matching the conditions.
*/
public function getStrings(array $conditions = array(), array $options = array());
/**
* Loads multiple string translation objects.
*
* @param array $conditions
* (optional) Array with conditions that will be used to filter the strings
* returned and may include all of the conditions defined by getStrings().
* @param array $options
* (optional) An associative array of additional options. It may contain
* any of the options defined by getStrings().
*
* @return \Drupal\locale\StringInterface[]
* Array of \Drupal\locale\StringInterface objects matching the conditions.
*
* @see \Drupal\locale\StringStorageInterface::getStrings()
*/
public function getTranslations(array $conditions = array(), array $options = array());
/**
* Loads string location information.
*
* @param array $conditions
* (optional) Array with conditions to filter the locations that may be any
* of the following elements:
* - 'sid', The string identifier.
* - 'type', The location type.
* - 'name', The location name.
*
* @return \Drupal\locale\StringInterface[]
* Array of \Drupal\locale\StringInterface objects matching the conditions.
*
* @see \Drupal\locale\StringStorageInterface::getStrings()
*/
public function getLocations(array $conditions = array());
/**
* Loads a string source object, fast query.
*
* These 'fast query' methods are the ones in the critical path and their
* implementation must be optimized for speed, as they may run many times
* in a single page request.
*
* @param array $conditions
* (optional) Array with conditions that will be used to filter the strings
* returned and may include all of the conditions defined by getStrings().
*
* @return \Drupal\locale\SourceString|null
* Minimal TranslationString object if found, NULL otherwise.
*/
public function findString(array $conditions);
/**
* Loads a string translation object, fast query.
*
* This function must only be used when actually translating strings as it
* will have the effect of updating the string version. For other purposes
* the getTranslations() method should be used instead.
*
* @param array $conditions
* (optional) Array with conditions that will be used to filter the strings
* returned and may include all of the conditions defined by getStrings().
*
* @return \Drupal\locale\TranslationString|null
* Minimal TranslationString object if found, NULL otherwise.
*/
public function findTranslation(array $conditions);
/**
* Save string object to storage.
*
* @param \Drupal\locale\StringInterface $string
* The string object.
*
* @return \Drupal\locale\StringStorageInterface
* The called object.
*
* @throws \Drupal\locale\StringStorageException
* In case of failures, an exception is thrown.
*/
public function save($string);
/**
* Delete string from storage.
*
* @param \Drupal\locale\StringInterface $string
* The string object.
*
* @return \Drupal\locale\StringStorageInterface
* The called object.
*
* @throws \Drupal\locale\StringStorageException
* In case of failures, an exception is thrown.
*/
public function delete($string);
/**
* Deletes source strings and translations using conditions.
*
* @param array $conditions
* Array with simple field conditions for source strings.
*/
public function deleteStrings($conditions);
/**
* Deletes translations using conditions.
*
* @param array $conditions
* Array with simple field conditions for string translations.
*/
public function deleteTranslations($conditions);
/**
* Counts source strings.
*
* @return int
* The number of source strings contained in the storage.
*/
public function countStrings();
/**
* Counts translations.
*
* @return array
* The number of translations for each language indexed by language code.
*/
public function countTranslations();
/**
* Creates a source string object bound to this storage but not saved.
*
* @param array $values
* (optional) Array with initial values. Defaults to empty array.
*
* @return \Drupal\locale\SourceString
* New source string object.
*/
public function createString($values = array());
/**
* Creates a string translation object bound to this storage but not saved.
*
* @param array $values
* (optional) Array with initial values. Defaults to empty array.
*
* @return \Drupal\locale\TranslationString
* New string translation object.
*/
public function createTranslation($values = array());
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleConfigManagerTest.
*/
namespace Drupal\locale\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\KernelTestBase;
/**
* Tests that the locale config manager operates correctly.
*
* @group locale
*/
class LocaleConfigManagerTest extends KernelTestBase {
/**
* A list of modules to install for this test.
*
* @var array
*/
public static $modules = array('language', 'locale', 'locale_test');
/**
* Tests hasTranslation().
*/
public function testHasTranslation() {
$this->installSchema('locale', array('locales_location', 'locales_source', 'locales_target'));
$this->installConfig(array('locale_test'));
$locale_config_manager = \Drupal::service('locale.config_manager');
$language = ConfigurableLanguage::createFromLangcode('de');
$language->save();
$result = $locale_config_manager->hasTranslation('locale_test.no_translation', $language->getId());
$this->assertFalse($result, 'There is no translation for locale_test.no_translation configuration.');
$result = $locale_config_manager->hasTranslation('locale_test.translation', $language->getId());
$this->assertTrue($result, 'There is a translation for locale_test.translation configuration.');
}
/**
* Tests getStringTranslation().
*/
public function testGetStringTranslation() {
$this->installSchema('locale', array('locales_location', 'locales_source', 'locales_target'));
$this->installConfig(array('locale_test'));
$locale_config_manager = \Drupal::service('locale.config_manager');
$language = ConfigurableLanguage::createFromLangcode('de');
$language->save();
$translation_before = $locale_config_manager->getStringTranslation('locale_test.no_translation', $language->getId(), 'Test', '');
$this->assertTrue($translation_before->isNew());
$translation_before->setString('translation')->save();
$translation_after = $locale_config_manager->getStringTranslation('locale_test.no_translation', $language->getId(), 'Test', '');
$this->assertFalse($translation_after->isNew());
$translation_after->setString('updated_translation')->save();
}
}

View file

@ -0,0 +1,170 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleConfigSubscriberForeignTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\Language\Language;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests default configuration handling with a foreign default language.
*
* @group locale
*/
class LocaleConfigSubscriberForeignTest extends LocaleConfigSubscriberTest {
/**
* {@inheritdoc}
*/
protected function defaultLanguageData() {
$data = Language::$defaultValues;
$data['id'] = 'hu';
$data['name'] = 'Hungarian';
return $data;
}
/**
* {@inheritdoc}
*/
protected function setUpLanguages() {
parent::setUpLanguages();
ConfigurableLanguage::createFromLangcode('hu')->save();
}
/**
* {@inheritdoc}
*/
protected function setUpLocale() {
parent::setUpLocale();
$this->setUpTranslation('locale_test.translation', 'test', 'English test', 'Hungarian test', 'hu', TRUE);
}
/**
* Tests that the language of default configuration was updated.
*/
public function testDefaultConfigLanguage() {
$this->assertEqual('hu', $this->configFactory->getEditable('locale_test.no_translation')->get('langcode'));
$this->assertEqual('hu', $this->configFactory->getEditable('locale_test.translation')->get('langcode'));
$this->assertEqual($this->configFactory->getEditable('locale_test.translation')->get('test'), 'Hungarian test');
}
/**
* Tests creating translations of shipped configuration.
*/
public function testCreateActiveTranslation() {
$config_name = 'locale_test.no_translation';
$this->saveLanguageActive($config_name, 'test', 'Test (Hungarian)', 'hu');
$this->assertTranslation($config_name, 'Test (Hungarian)', 'hu');
}
/**
* Tests importing community translations of shipped configuration.
*/
public function testLocaleCreateActiveTranslation() {
$config_name = 'locale_test.no_translation';
$this->saveLocaleTranslationData($config_name, 'test', 'Test', 'Test (Hungarian)', 'hu', TRUE);
$this->assertTranslation($config_name, 'Test (Hungarian)', 'hu', FALSE);
}
/**
* Tests updating translations of shipped configuration.
*/
public function testUpdateActiveTranslation() {
$config_name = 'locale_test.translation';
$this->saveLanguageActive($config_name, 'test', 'Updated Hungarian test', 'hu');
$this->assertTranslation($config_name, 'Updated Hungarian test', 'hu');
}
/**
* Tests updating community translations of shipped configuration.
*/
public function testLocaleUpdateActiveTranslation() {
$config_name = 'locale_test.translation';
$this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated Hungarian test', 'hu', TRUE);
$this->assertTranslation($config_name, 'Updated Hungarian test', 'hu', FALSE);
}
/**
* Tests deleting a translation override.
*/
public function testDeleteTranslation() {
$config_name = 'locale_test.translation';
$this->deleteLanguageOverride($config_name, 'test', 'English test', 'de');
// The German translation in this case will be forced to the Hungarian
// source so its not overwritten with locale data later.
$this->assertTranslation($config_name, 'Hungarian test', 'de');
}
/**
* Tests deleting translations of shipped configuration.
*/
public function testDeleteActiveTranslation() {
$config_name = 'locale_test.translation';
$this->configFactory->getEditable($config_name)->delete();
// Deleting active configuration should not change the locale translation.
$this->assertTranslation($config_name, 'Hungarian test', 'hu', FALSE);
}
/**
* Tests deleting community translations of shipped configuration.
*/
public function testLocaleDeleteActiveTranslation() {
$config_name = 'locale_test.translation';
$this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'hu');
// Deleting the locale translation should not change active config.
$this->assertEqual($this->configFactory->getEditable($config_name)->get('test'), 'Hungarian test');
}
/**
* Tests that adding English creates a translation override.
*/
public function testEnglish() {
$config_name = 'locale_test.translation';
ConfigurableLanguage::createFromLangcode('en')->save();
// Adding a language on the UI would normally call updateConfigTranslations.
$this->localeConfigManager->updateConfigTranslations(array($config_name), array('en'));
$this->assertConfigOverride($config_name, 'test', 'English test', 'en');
$this->configFactory->getEditable('locale.settings')->set('translate_english', TRUE)->save();
$this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated English test', 'en');
$this->assertTranslation($config_name, 'Updated English test', 'en', FALSE);
$this->saveLanguageOverride($config_name, 'test', 'Updated English', 'en');
$this->assertTranslation($config_name, 'Updated English', 'en');
$this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'en');
$this->assertNoConfigOverride($config_name, 'en');
}
/**
* Saves a language override.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $value
* The configuration value to save.
* @param string $langcode
* The language code.
*/
protected function saveLanguageActive($config_name, $key, $value, $langcode) {
$this
->configFactory
->getEditable($config_name)
->set($key, $value)
->save();
$this->assertActiveConfig($config_name, $key, $value, $langcode);
}
}

View file

@ -0,0 +1,493 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleConfigSubscriberTest.
*/
namespace Drupal\locale\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\locale\Locale;
use Drupal\locale\StringInterface;
use Drupal\locale\TranslationString;
use Drupal\simpletest\KernelTestBase;
/**
* Tests that shipped configuration translations are updated correctly.
*
* @group locale
*/
class LocaleConfigSubscriberTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['language', 'locale', 'system'];
/**
* The configurable language manager used in this test.
*
* @var \Drupal\language\ConfigurableLanguageManagerInterface
*/
protected $languageManager;
/**
* The configuration factory used in this test.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The string storage used in this test.
*
* @var \Drupal\locale\StringStorageInterface;
*/
protected $stringStorage;
/**
* The locale configuration manager used in this test.
*
* @var \Drupal\locale\LocaleConfigManager
*/
protected $localeConfigManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->setUpDefaultLanguage();
$this->installSchema('locale', ['locales_source', 'locales_target', 'locales_location']);
$this->installSchema('system', ['queue']);
$this->setupLanguages();
$this->enableModules(['locale_test']);
$this->installConfig(['locale_test']);
// Simulate this hook invoked which would happen if in a non-kernel test
// or normal environment.
// @see locale_modules_installed()
// @see locale_system_update()
locale_system_set_config_langcodes();
$langcodes = array_keys(\Drupal::languageManager()->getLanguages());
$names = \Drupal\locale\Locale::config()->getComponentNames();
Locale::config()->updateConfigTranslations($names, $langcodes);
$this->configFactory = $this->container->get('config.factory');
$this->stringStorage = $this->container->get('locale.storage');
$this->localeConfigManager = $this->container->get('locale.config_manager');
$this->languageManager = $this->container->get('language_manager');
$this->setUpLocale();
}
/**
* Sets up default language for this test.
*/
protected function setUpDefaultLanguage() {
// Keep the default English.
}
/**
* Sets up languages needed for this test.
*/
protected function setUpLanguages() {
ConfigurableLanguage::createFromLangcode('de')->save();
}
/**
* Sets up the locale storage strings to be in line with configuration.
*/
protected function setUpLocale() {
// Set up the locale database the same way we have in the config samples.
$this->setUpNoTranslation('locale_test.no_translation', 'test', 'Test', 'de');
$this->setUpTranslation('locale_test.translation', 'test', 'English test', 'German test', 'de');
}
/**
* Tests creating translations of shipped configuration.
*/
public function testCreateTranslation() {
$config_name = 'locale_test.no_translation';
$this->saveLanguageOverride($config_name, 'test', 'Test (German)', 'de');
$this->assertTranslation($config_name, 'Test (German)', 'de');
}
/**
* Tests importing community translations of shipped configuration.
*/
public function testLocaleCreateTranslation() {
$config_name = 'locale_test.no_translation';
$this->saveLocaleTranslationData($config_name, 'test', 'Test', 'Test (German)', 'de');
$this->assertTranslation($config_name, 'Test (German)', 'de', FALSE);
}
/**
* Tests updating translations of shipped configuration.
*/
public function testUpdateTranslation() {
$config_name = 'locale_test.translation';
$this->saveLanguageOverride($config_name, 'test', 'Updated German test', 'de');
$this->assertTranslation($config_name, 'Updated German test', 'de');
}
/**
* Tests updating community translations of shipped configuration.
*/
public function testLocaleUpdateTranslation() {
$config_name = 'locale_test.translation';
$this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated German test', 'de');
$this->assertTranslation($config_name, 'Updated German test', 'de', FALSE);
}
/**
* Tests deleting translations of shipped configuration.
*/
public function testDeleteTranslation() {
$config_name = 'locale_test.translation';
$this->deleteLanguageOverride($config_name, 'test', 'English test', 'de');
// Instead of deleting the translation, we need to keep a translation with
// the source value and mark it as customized to prevent the deletion being
// reverted by importing community translations.
$this->assertTranslation($config_name, 'English test', 'de');
}
/**
* Tests deleting community translations of shipped configuration.
*/
public function testLocaleDeleteTranslation() {
$config_name = 'locale_test.translation';
$this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'de');
$this->assertNoTranslation($config_name, 'de');
}
/**
* Sets up a configuration string without a translation.
*
* The actual configuration is already available by installing locale_test
* module, as it is done in LocaleConfigSubscriberTest::setUp(). This sets up
* the necessary source string and verifies that everything is as expected to
* avoid false positives.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source
* The source string.
* @param string $langcode
* The language code.
*/
protected function setUpNoTranslation($config_name, $key, $source, $langcode) {
$this->localeConfigManager->updateConfigTranslations(array($config_name), array($langcode));
$this->assertNoConfigOverride($config_name, $key, $source, $langcode);
$this->assertNoTranslation($config_name, $langcode);
}
/**
* Sets up a configuration string with a translation.
*
* The actual configuration is already available by installing locale_test
* module, as it is done in LocaleConfigSubscriberTest::setUp(). This sets up
* the necessary source and translation strings and verifies that everything
* is as expected to avoid false positives.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source
* The source string.
* @param string $translation
* The translation string.
* @param string $langcode
* The language code.
* @param bool $is_active
* Whether the update will affect the active configuration.
*/
protected function setUpTranslation($config_name, $key, $source, $translation, $langcode, $is_active = FALSE) {
// Create source and translation strings for the configuration value and add
// the configuration name as a location. This would be performed by
// locale_translate_batch_import() invoking
// LocaleConfigManager::updateConfigTranslations() normally.
$this->localeConfigManager->reset();
$this->localeConfigManager
->getStringTranslation($config_name, $langcode, $source, '')
->setString($translation)
->setCustomized(FALSE)
->save();
$this->configFactory->reset($config_name);
$this->localeConfigManager->reset();
$this->localeConfigManager->updateConfigTranslations(array($config_name), array($langcode));
if ($is_active) {
$this->assertActiveConfig($config_name, $key, $translation, $langcode);
}
else {
$this->assertConfigOverride($config_name, $key, $translation, $langcode);
}
$this->assertTranslation($config_name, $translation, $langcode, FALSE);
}
/**
* Saves a language override.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $value
* The configuration value to save.
* @param string $langcode
* The language code.
*/
protected function saveLanguageOverride($config_name, $key, $value, $langcode) {
$translation_override = $this->languageManager
->getLanguageConfigOverride($langcode, $config_name);
$translation_override
->set($key, $value)
->save();
$this->configFactory->reset($config_name);
$this->assertConfigOverride($config_name, $key, $value, $langcode);
}
/**
* Saves translation data from locale module.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source
* The source string.
* @param string $translation
* The translation string to save.
* @param string $langcode
* The language code.
* @param bool $is_active
* Whether the update will affect the active configuration.
*/
protected function saveLocaleTranslationData($config_name, $key, $source, $translation, $langcode, $is_active = FALSE) {
$this->localeConfigManager->reset();
$this->localeConfigManager
->getStringTranslation($config_name, $langcode, $source, '')
->setString($translation)
->save();
$this->localeConfigManager->reset();
$this->localeConfigManager->updateConfigTranslations(array($config_name), array($langcode));
$this->configFactory->reset($config_name);
if ($is_active) {
$this->assertActiveConfig($config_name, $key, $translation, $langcode);
}
else {
$this->assertConfigOverride($config_name, $key, $translation, $langcode);
}
}
/**
* Deletes a language override.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source_value
* The source configuration value to verify the correct value is returned
* from the configuration factory after the deletion.
* @param string $langcode
* The language code.
*/
protected function deleteLanguageOverride($config_name, $key, $source_value, $langcode) {
$translation_override = $this->languageManager
->getLanguageConfigOverride($langcode, $config_name);
$translation_override
->clear($key)
->save();
$this->configFactory->reset($config_name);
$this->assertNoConfigOverride($config_name, $key, $source_value, $langcode);
}
/**
* Deletes translation data from locale module.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source_value
* The source configuration value to verify the correct value is returned
* from the configuration factory after the deletion.
* @param string $langcode
* The language code.
*/
protected function deleteLocaleTranslationData($config_name, $key, $source_value, $langcode) {
$this->localeConfigManager
->getStringTranslation($config_name, $langcode, $source_value, '')
->delete();
$this->localeConfigManager->reset();
$this->localeConfigManager->updateConfigTranslations(array($config_name), array($langcode));
$this->configFactory->reset($config_name);
$this->assertNoConfigOverride($config_name, $key, $source_value, $langcode);
}
/**
* Ensures configuration override is not present anymore.
*
* @param string $config_name
* The configuration name.
* @param string $langcode
* The language code.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertNoConfigOverride($config_name, $langcode) {
$config_langcode = $this->configFactory->getEditable($config_name)->get('langcode');
$override = $this->languageManager->getLanguageConfigOverride($langcode, $config_name);
return $this->assertNotEqual($config_langcode, $langcode) && $this->assertEqual($override->isNew(), TRUE);
}
/**
* Ensures configuration was saved correctly.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $value
* The configuration value.
* @param string $langcode
* The language code.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertConfigOverride($config_name, $key, $value, $langcode) {
$config_langcode = $this->configFactory->getEditable($config_name)->get('langcode');
$override = $this->languageManager->getLanguageConfigOverride($langcode, $config_name);
return $this->assertNotEqual($config_langcode, $langcode) && $this->assertEqual($override->get($key), $value);
}
/**
* Ensures configuration was saved correctly.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $value
* The configuration value.
* @param string $langcode
* The language code.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertActiveConfig($config_name, $key, $value, $langcode) {
$config = $this->configFactory->getEditable($config_name);
return
$this->assertEqual($config->get('langcode'), $langcode) &&
$this->assertIdentical($config->get($key), $value);
}
/**
* Ensures no translation exists.
*
* @param string $config_name
* The configuration name.
* @param string $langcode
* The language code.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertNoTranslation($config_name, $langcode) {
$strings = $this->stringStorage->getTranslations([
'type' => 'configuration',
'name' => $config_name,
'language' => $langcode,
'translated' => TRUE,
]);
return $this->assertIdentical([], $strings);
}
/**
* Ensures a translation exists and is marked as customized.
*
* @param string $config_name
* The configuration name.
* @param string $translation
* The translation.
* @param string $langcode
* The language code.
* @param bool $customized
* Whether or not the string should be asserted to be customized or not
* customized.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertTranslation($config_name, $translation, $langcode, $customized = TRUE) {
// Make sure a string exists.
$strings = $this->stringStorage->getTranslations([
'type' => 'configuration',
'name' => $config_name,
'language' => $langcode,
'translated' => TRUE,
]);
$pass = $this->assertIdentical(1, count($strings));
$string = reset($strings);
if ($this->assertTrue($string instanceof StringInterface)) {
/** @var \Drupal\locale\StringInterface $string */
$pass = $pass && $this->assertIdentical($translation, $string->getString());
$pass = $pass && $this->assertTrue($string->isTranslation());
if ($this->assertTrue($string instanceof TranslationString)) {
/** @var \Drupal\locale\TranslationString $string */
// Make sure the string is marked as customized so that it does not get
// overridden when the string translations are updated.
return $pass && $this->assertEqual($customized, $string->customized);
}
}
return FALSE;
}
}

View file

@ -0,0 +1,84 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleConfigTranslationImportTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Core\Url;
/**
* Tests translation update's effects on configuration translations.
*
* @group locale
*/
class LocaleConfigTranslationImportTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'update', 'locale_test_translate');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'administer permissions'));
$this->drupalLogin($admin_user);
// Update module should not go out to d.o to check for updates. We override
// the url to an invalid update source. No update data will be found.
$this->config('update.settings')->set('fetch.url', (string) Url::fromRoute('<front>')->setAbsolute()->toString())->save();
}
/**
* Test update changes configuration translations if enabled after language.
*/
public function testConfigTranslationImport() {
// Add a language. The Afrikaans translation file of locale_test_translate
// (test.af.po) has been prepared with a configuration translation.
ConfigurableLanguage::createFromLangcode('af')->save();
// Enable locale module.
$this->container->get('module_installer')->install(array('locale'));
$this->resetAll();
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->save();
// Add translation permissions now that the locale module has been enabled.
$edit = array(
'authenticated[translate interface]' => 'translate interface',
);
$this->drupalPostForm('admin/people/permissions', $edit, t('Save permissions'));
// Check and update the translation status. This will import the Afrikaans
// translations of locale_test_translate module.
$this->drupalGet('admin/reports/translations/check');
// Override the Drupal core translation status to be up to date.
// Drupal core should not be a subject in this test.
$status = locale_translation_get_status();
$status['drupal']['af']->type = 'current';
\Drupal::state()->set('locale.translation_status', $status);
$this->drupalPostForm('admin/reports/translations', array(), t('Update translations'));
// Check if configuration translations have been imported.
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'system.maintenance');
$this->assertEqual($override->get('message'), 'Ons is tans besig met onderhoud op @site. Wees asseblief geduldig, ons sal binnekort weer terug wees.');
}
}

View file

@ -0,0 +1,251 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleConfigTranslationTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\simpletest\WebTestBase;
use Drupal\core\language\languageInterface;
/**
* Tests translation of configuration strings.
*
* @group locale
*/
class LocaleConfigTranslationTest extends WebTestBase {
/**
* The language code used.
*
* @var string
*/
protected $langcode;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale', 'contact', 'contact_test');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Add a default locale storage for all these tests.
$this->storage = $this->container->get('locale.storage');
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->save();
// Add custom language.
$this->langcode = 'xx';
$admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface', 'administer modules', 'access site-wide contact form', 'administer contact forms', 'administer site configuration'));
$this->drupalLogin($admin_user);
$name = $this->randomMachineName(16);
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $this->langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Set path prefix.
$edit = ["prefix[$this->langcode]" => $this->langcode];
$this->drupalPostForm('admin/config/regional/language/detection/url', $edit, t('Save configuration'));
}
/**
* Tests basic configuration translation.
*/
public function testConfigTranslation() {
// Check that the maintenance message exists and create translation for it.
$source = '@site is currently under maintenance. We should be back shortly. Thank you for your patience.';
$string = $this->storage->findString(array('source' => $source, 'context' => '', 'type' => 'configuration'));
$this->assertTrue($string, 'Configuration strings have been created upon installation.');
// Translate using the UI so configuration is refreshed.
$message = $this->randomMachineName(20);
$search = array(
'string' => $string->source,
'langcode' => $this->langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$textareas = $this->xpath('//textarea');
$textarea = current($textareas);
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $message,
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Get translation and check we've only got the message.
$translation = \Drupal::languageManager()->getLanguageConfigOverride($this->langcode, 'system.maintenance')->get();
$this->assertEqual(count($translation), 1, 'Got the right number of properties after translation.');
$this->assertEqual($translation['message'], $message);
// Check default medium date format exists and create a translation for it.
$string = $this->storage->findString(array('source' => 'D, m/d/Y - H:i', 'context' => 'PHP date format', 'type' => 'configuration'));
$this->assertTrue($string, 'Configuration date formats have been created upon installation.');
// Translate using the UI so configuration is refreshed.
$search = array(
'string' => $string->source,
'langcode' => $this->langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$textareas = $this->xpath('//textarea');
$textarea = current($textareas);
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => 'D',
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
$translation = \Drupal::languageManager()->getLanguageConfigOverride($this->langcode, 'core.date_format.medium')->get();
$this->assertEqual($translation['pattern'], 'D', 'Got the right date format pattern after translation.');
// Formatting the date 8 / 27 / 1985 @ 13:37 EST with pattern D should
// display "Tue".
$formatted_date = format_date(494015820, $type = 'medium', NULL, NULL, $this->langcode);
$this->assertEqual($formatted_date, 'Tue', 'Got the right formatted date using the date format translation pattern.');
// Assert strings from image module config are not available.
$string = $this->storage->findString(array('source' => 'Medium (220×220)', 'context' => '', 'type' => 'configuration'));
$this->assertFalse($string, 'Configuration strings have been created upon installation.');
// Enable the image module.
$this->drupalPostForm('admin/modules', array('modules[Field types][image][enable]' => "1"), t('Save configuration'));
$this->rebuildContainer();
$string = $this->storage->findString(array('source' => 'Medium (220×220)', 'context' => '', 'type' => 'configuration'));
$this->assertTrue($string, 'Configuration strings have been created upon installation.');
$locations = $string->getLocations();
$this->assertTrue(isset($locations['configuration']) && isset($locations['configuration']['image.style.medium']), 'Configuration string has been created with the right location');
// Check the string is unique and has no translation yet.
$translations = $this->storage->getTranslations(['language' => $this->langcode, 'type' => 'configuration', 'name' => 'image.style.medium']);
$this->assertEqual(count($translations), 1);
$translation = reset($translations);
$this->assertEqual($translation->source, $string->source);
$this->assertTrue(empty($translation->translation));
// Translate using the UI so configuration is refreshed.
$image_style_label = $this->randomMachineName(20);
$search = array(
'string' => $string->source,
'langcode' => $this->langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $image_style_label,
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Check the right single translation has been created.
$translations = $this->storage->getTranslations(['language' => $this->langcode, 'type' => 'configuration', 'name' => 'image.style.medium']);
$translation = reset($translations);
$this->assertTrue(count($translations) == 1 && $translation->source == $string->source && $translation->translation == $image_style_label, 'Got only one translation for image configuration.');
// Try more complex configuration data.
$translation = \Drupal::languageManager()->getLanguageConfigOverride($this->langcode, 'image.style.medium')->get();
$this->assertEqual($translation['label'], $image_style_label, 'Got the right translation for image style name after translation');
// Uninstall the module.
$this->drupalPostForm('admin/modules/uninstall', array('uninstall[image]' => "image"), t('Uninstall'));
$this->drupalPostForm(NULL, array(), t('Uninstall'));
// Ensure that the translated configuration has been removed.
$override = \Drupal::languageManager()->getLanguageConfigOverride('xx', 'image.style.medium');
$this->assertTrue($override->isNew(), 'Translated configuration for image module removed.');
// Translate default category using the UI so configuration is refreshed.
$category_label = $this->randomMachineName(20);
$search = array(
'string' => 'Website feedback',
'langcode' => $this->langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $category_label,
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Check if this category displayed in this language will use the
// translation. This test ensures the entity loaded from the request
// upcasting will already work.
$this->drupalGet($this->langcode . '/contact/feedback');
$this->assertText($category_label);
// Check if the UI does not show the translated String.
$this->drupalGet('admin/structure/contact/manage/feedback');
$this->assertFieldById('edit-label', 'Website feedback', 'Translation is not loaded for Edit Form.');
}
/**
* Test translatability of optional configuration in locale.
*/
public function testOptionalConfiguration() {
$this->assertNodeConfig(FALSE, FALSE);
// Enable the node module.
$this->drupalPostForm('admin/modules', ['modules[Core][node][enable]' => "1"], t('Save configuration'));
$this->drupalPostForm(NULL, [], t('Continue'));
$this->rebuildContainer();
$this->assertNodeConfig(TRUE, FALSE);
// Enable the views module (which node provides some optional config for).
$this->drupalPostForm('admin/modules', ['modules[Core][views][enable]' => "1"], t('Save configuration'));
$this->rebuildContainer();
$this->assertNodeConfig(TRUE, TRUE);
}
/**
* Check that node configuration source strings are made available in locale.
*
* @param bool $required
* Whether to assume a sample of the required default configuration is
* present.
* @param bool $optional
* Whether to assume a sample of the optional default configuration is
* present.
*/
protected function assertNodeConfig($required, $optional) {
// Check the required default configuration in node module.
$string = $this->storage->findString(['source' => 'Make content sticky', 'context' => '', 'type' => 'configuration']);
if ($required) {
$this->assertFalse($this->config('system.action.node_make_sticky_action')->isNew());
$this->assertTrue($string, 'Node action text can be found with node module.');
}
else {
$this->assertTrue($this->config('system.action.node_make_sticky_action')->isNew());
$this->assertFalse($string, 'Node action text can not be found without node module.');
}
// Check the optional default configuration in node module.
$string = $this->storage->findString(['source' => 'No front page content has been created yet.', 'context' => '', 'type' => 'configuration']);
if ($optional) {
$this->assertFalse($this->config('views.view.frontpage')->isNew());
$this->assertTrue($string, 'Node view text can be found with node and views modules.');
}
else {
$this->assertTrue($this->config('views.view.frontpage')->isNew());
$this->assertFalse($string, 'Node view text can not be found without node and/or views modules.');
}
}
}

View file

@ -0,0 +1,202 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleContentTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\Language\LanguageInterface;
/**
* Tests you can enable multilingual support on content types and configure a
* language for a node.
*
* @group locale
*/
class LocaleContentTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'locale');
/**
* Verifies that machine name fields are always LTR.
*/
public function testMachineNameLTR() {
// User to add and remove language.
$admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages', 'administer site configuration'));
// Log in as admin.
$this->drupalLogin($admin_user);
// Verify that the machine name field is LTR for a new content type.
$this->drupalGet('admin/structure/types/add');
$this->assertFieldByXpath('//input[@name="type" and @dir="ltr"]', NULL, 'The machine name field is LTR when no additional language is configured.');
// Install the Arabic language (which is RTL) and configure as the default.
$edit = array();
$edit['predefined_langcode'] = 'ar';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$edit = array(
'site_default_language' => 'ar',
);
$this->drupalPostForm('admin/config/regional/language', $edit, t('Save configuration'));
// Verify that the machine name field is still LTR for a new content type.
$this->drupalGet('admin/structure/types/add');
$this->assertFieldByXpath('//input[@name="type" and @dir="ltr"]', NULL, 'The machine name field is LTR when the default language is RTL.');
}
/**
* Test if a content type can be set to multilingual and language is present.
*/
public function testContentTypeLanguageConfiguration() {
$type1 = $this->drupalCreateContentType();
$type2 = $this->drupalCreateContentType();
// User to add and remove language.
$admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages'));
// User to create a node.
$web_user = $this->drupalCreateUser(array("create {$type1->id()} content", "create {$type2->id()} content", "edit any {$type2->id()} content"));
// Add custom language.
$this->drupalLogin($admin_user);
// Code for the language.
$langcode = 'xx';
// The English name for the language.
$name = $this->randomMachineName(16);
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Set the content type to use multilingual support.
$this->drupalGet("admin/structure/types/manage/{$type2->id()}");
$this->assertText(t('Language settings'), 'Multilingual support widget present on content type configuration form.');
$edit = array(
'language_configuration[language_alterable]' => TRUE,
);
$this->drupalPostForm("admin/structure/types/manage/{$type2->id()}", $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => $type2->label())));
$this->drupalLogout();
\Drupal::languageManager()->reset();
// Verify language selection is not present on the node add form.
$this->drupalLogin($web_user);
$this->drupalGet("node/add/{$type1->id()}");
// Verify language select list is not present.
$this->assertNoFieldByName('langcode[0][value]', NULL, 'Language select not present on the node add form.');
// Verify language selection appears on the node add form.
$this->drupalGet("node/add/{$type2->id()}");
// Verify language select list is present.
$this->assertFieldByName('langcode[0][value]', NULL, 'Language select present on the node add form.');
// Ensure language appears.
$this->assertText($name, 'Language present.');
// Create a node.
$node_title = $this->randomMachineName();
$node_body = $this->randomMachineName();
$edit = array(
'type' => $type2->id(),
'title' => $node_title,
'body' => array(array('value' => $node_body)),
'langcode' => $langcode,
);
$node = $this->drupalCreateNode($edit);
// Edit the content and ensure correct language is selected.
$path = 'node/' . $node->id() . '/edit';
$this->drupalGet($path);
$this->assertRaw('<option value="' . $langcode . '" selected="selected">' . $name . '</option>', 'Correct language selected.');
// Ensure we can change the node language.
$edit = array(
'langcode[0][value]' => 'en',
);
$this->drupalPostForm($path, $edit, t('Save'));
$this->assertRaw(t('%title has been updated.', array('%title' => $node_title)));
$this->drupalLogout();
}
/**
* Test if a dir and lang tags exist in node's attributes.
*/
public function testContentTypeDirLang() {
$type = $this->drupalCreateContentType();
// User to add and remove language.
$admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages'));
// User to create a node.
$web_user = $this->drupalCreateUser(array("create {$type->id()} content", "edit own {$type->id()} content"));
// Login as admin.
$this->drupalLogin($admin_user);
// Install Arabic language.
$edit = array();
$edit['predefined_langcode'] = 'ar';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// Install Spanish language.
$edit = array();
$edit['predefined_langcode'] = 'es';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
\Drupal::languageManager()->reset();
// Set the content type to use multilingual support.
$this->drupalGet("admin/structure/types/manage/{$type->id()}");
$edit = array(
'language_configuration[language_alterable]' => TRUE,
);
$this->drupalPostForm("admin/structure/types/manage/{$type->id()}", $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => $type->label())));
$this->drupalLogout();
// Login as web user to add new node.
$this->drupalLogin($web_user);
// Create three nodes: English, Arabic and Spanish.
$nodes = array();
foreach (array('en', 'es', 'ar') as $langcode) {
$nodes[$langcode] = $this->drupalCreateNode(array(
'langcode' => $langcode,
'type' => $type->id(),
'promote' => NODE_PROMOTED,
));
}
// Check if English node does not have lang tag.
$this->drupalGet('node/' . $nodes['en']->id());
$element = $this->cssSelect('article.node[lang="en"]');
$this->assertTrue(empty($element), 'The lang tag has not been assigned to the English node.');
// Check if English node does not have dir tag.
$element = $this->cssSelect('article.node[dir="ltr"]');
$this->assertTrue(empty($element), 'The dir tag has not been assigned to the English node.');
// Check if Arabic node has lang="ar" & dir="rtl" tags.
$this->drupalGet('node/' . $nodes['ar']->id());
$element = $this->cssSelect('article.node[lang="ar"][dir="rtl"]');
$this->assertTrue(!empty($element), 'The lang and dir tags have been assigned correctly to the Arabic node.');
// Check if Spanish node has lang="es" tag.
$this->drupalGet('node/' . $nodes['es']->id());
$element = $this->cssSelect('article.node[lang="es"]');
$this->assertTrue(!empty($element), 'The lang tag has been assigned correctly to the Spanish node.');
// Check if Spanish node does not have dir="ltr" tag.
$element = $this->cssSelect('article.node[lang="es"][dir="ltr"]');
$this->assertTrue(empty($element), 'The dir tag has not been assigned to the Spanish node.');
}
}

View file

@ -0,0 +1,181 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleExportTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the exportation of locale files.
*
* @group locale
*/
class LocaleExportTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
/**
* A user able to create languages and export translations.
*/
protected $adminUser = NULL;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
$this->drupalLogin($this->adminUser);
// Copy test po files to the translations directory.
file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.de.po', 'translations://', FILE_EXISTS_REPLACE);
file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.xx.po', 'translations://', FILE_EXISTS_REPLACE);
}
/**
* Test exportation of translations.
*/
public function testExportTranslation() {
// First import some known translations.
// This will also automatically add the 'fr' language.
$name = tempnam('temporary://', "po_") . '.po';
file_put_contents($name, $this->getPoFile());
$this->drupalPostForm('admin/config/regional/translate/import', array(
'langcode' => 'fr',
'files[file]' => $name,
), t('Import'));
drupal_unlink($name);
// Get the French translations.
$this->drupalPostForm('admin/config/regional/translate/export', array(
'langcode' => 'fr',
), t('Export'));
// Ensure we have a translation file.
$this->assertRaw('# French translation of Drupal', 'Exported French translation file.');
// Ensure our imported translations exist in the file.
$this->assertRaw('msgstr "lundi"', 'French translations present in exported file.');
// Import some more French translations which will be marked as customized.
$name = tempnam('temporary://', "po2_") . '.po';
file_put_contents($name, $this->getCustomPoFile());
$this->drupalPostForm('admin/config/regional/translate/import', array(
'langcode' => 'fr',
'files[file]' => $name,
'customized' => 1,
), t('Import'));
drupal_unlink($name);
// Create string without translation in the locales_source table.
$this->container
->get('locale.storage')
->createString()
->setString('February')
->save();
// Export only customized French translations.
$this->drupalPostForm('admin/config/regional/translate/export', array(
'langcode' => 'fr',
'content_options[not_customized]' => FALSE,
'content_options[customized]' => TRUE,
'content_options[not_translated]' => FALSE,
), t('Export'));
// Ensure we have a translation file.
$this->assertRaw('# French translation of Drupal', 'Exported French translation file with only customized strings.');
// Ensure the customized translations exist in the file.
$this->assertRaw('msgstr "janvier"', 'French custom translation present in exported file.');
// Ensure no untranslated strings exist in the file.
$this->assertNoRaw('msgid "February"', 'Untranslated string not present in exported file.');
// Export only untranslated French translations.
$this->drupalPostForm('admin/config/regional/translate/export', array(
'langcode' => 'fr',
'content_options[not_customized]' => FALSE,
'content_options[customized]' => FALSE,
'content_options[not_translated]' => TRUE,
), t('Export'));
// Ensure we have a translation file.
$this->assertRaw('# French translation of Drupal', 'Exported French translation file with only untranslated strings.');
// Ensure no customized translations exist in the file.
$this->assertNoRaw('msgstr "janvier"', 'French custom translation not present in exported file.');
// Ensure the untranslated strings exist in the file, and with right quotes.
$this->assertRaw($this->getUntranslatedString(), 'Empty string present in exported file.');
}
/**
* Test exportation of translation template file.
*/
public function testExportTranslationTemplateFile() {
// Load an admin page with JavaScript so _drupal_add_library() fires at
// least once and _locale_parse_js_file() gets to run at least once so that
// the locales_source table gets populated with something.
$this->drupalGet('admin/config/regional/language');
// Get the translation template file.
$this->drupalPostForm('admin/config/regional/translate/export', array(), t('Export'));
// Ensure we have a translation file.
$this->assertRaw('# LANGUAGE translation of PROJECT', 'Exported translation template file.');
}
/**
* Helper function that returns a proper .po file.
*/
public function getPoFile() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "Monday"
msgstr "lundi"
EOF;
}
/**
* Helper function that returns a .po file which strings will be marked
* as customized.
*/
public function getCustomPoFile() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "January"
msgstr "janvier"
EOF;
}
/**
* Returns a .po file fragment with an untranslated string.
*
* @return string
* A .po file fragment with an untranslated string.
*/
public function getUntranslatedString() {
return <<< EOF
msgid "February"
msgstr ""
EOF;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleFileSystemFormTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the locale functionality in the altered file settings form.
*
* @group locale
*/
class LocaleFileSystemFormTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system');
/**
* {@inheritdoc}
*/
protected function setUp(){
parent::setUp();
$account = $this->drupalCreateUser(array('administer site configuration'));
$this->drupalLogin($account);
}
/**
* Tests translation directory settings on the file settings form.
*/
function testFileConfigurationPage() {
// By default there should be no setting for the translation directory.
$this->drupalGet('admin/config/media/file-system');
$this->assertNoFieldByName('translation_path');
// With locale module installed, the setting should appear.
$module_installer = $this->container->get('module_installer');
$module_installer->install(['locale']);
$this->rebuildContainer();
$this->drupalGet('admin/config/media/file-system');
$this->assertFieldByName('translation_path');
// The setting should persist.
$translation_path = $this->publicFilesDirectory . '/translations_changed';
$fields = array(
'translation_path' => $translation_path
);
$this->drupalPostForm(NULL, $fields, t('Save configuration'));
$this->drupalGet('admin/config/media/file-system');
$this->assertFieldByName('translation_path', $translation_path);
$this->assertEqual($translation_path, $this->config('locale.settings')->get('translation.path'));
}
}

View file

@ -0,0 +1,644 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleImportFunctionalTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\Language\LanguageInterface;
/**
* Tests the import of locale files.
*
* @group locale
*/
class LocaleImportFunctionalTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale', 'dblog');
/**
* A user able to create languages and import translations.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* A user able to create languages, import translations and access site
* reports.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUserAccessSiteReports;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Copy test po files to the translations directory.
file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.de.po', 'translations://', FILE_EXISTS_REPLACE);
file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.xx.po', 'translations://', FILE_EXISTS_REPLACE);
$this->adminUser = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
$this->adminUserAccessSiteReports = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages', 'access site reports'));
$this->drupalLogin($this->adminUser);
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->save();
}
/**
* Test import of standalone .po files.
*/
public function testStandalonePoFile() {
// Try importing a .po file.
$this->importPoFile($this->getPoFile(), array(
'langcode' => 'fr',
));
$this->config('locale.settings');
// The import should automatically create the corresponding language.
$this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), 'The language has been automatically created.');
// The import should have created 8 strings.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.');
// This import should have saved plural forms to have 2 variants.
$locale_plurals = \Drupal::state()->get('locale.translation.plurals') ?: array();
$this->assert($locale_plurals['fr']['plurals'] == 2, 'Plural number initialized.');
// Ensure we were redirected correctly.
$this->assertUrl(\Drupal::url('locale.translate_page', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
// Try importing a .po file with invalid tags.
$this->importPoFile($this->getBadPoFile(), array(
'langcode' => 'fr',
));
// The import should have created 1 string and rejected 2.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.');
$skip_message = \Drupal::translation()->formatPlural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => \Drupal::url('dblog.overview')));
$this->assertRaw($skip_message, 'Unsafe strings were skipped.');
// Repeat the process with a user that can access site reports, and this
// time the different warnings must contain links to the log.
$this->drupalLogin($this->adminUserAccessSiteReports);
// Try importing a .po file with invalid tags.
$this->importPoFile($this->getBadPoFile(), array(
'langcode' => 'fr',
));
$skip_message = \Drupal::translation()->formatPlural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => \Drupal::url('dblog.overview')));
$this->assertRaw($skip_message, 'Unsafe strings were skipped.');
// Check empty files import with a user that cannot access site reports..
$this->drupalLogin($this->adminUser);
// Try importing a zero byte sized .po file.
$this->importPoFile($this->getEmptyPoFile(), array(
'langcode' => 'fr',
));
// The import should have created 0 string and rejected 0.
$this->assertRaw(t('One translation file could not be imported. See the log for details.'), 'The empty translation file import reported no translations imported.');
// Repeat the process with a user that can access site reports, and this
// time the different warnings must contain links to the log.
$this->drupalLogin($this->adminUserAccessSiteReports);
// Try importing a zero byte sized .po file.
$this->importPoFile($this->getEmptyPoFile(), array(
'langcode' => 'fr',
));
// The import should have created 0 string and rejected 0.
$this->assertRaw(t('One translation file could not be imported. <a href="@url">See the log</a> for details.', array('@url' => \Drupal::url('dblog.overview'))), 'The empty translation file import reported no translations imported.');
// Try importing a .po file which doesn't exist.
$name = $this->randomMachineName(16);
$this->drupalPostForm('admin/config/regional/translate/import', array(
'langcode' => 'fr',
'files[file]' => $name,
), t('Import'));
$this->assertUrl(\Drupal::url('locale.translate_import', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
$this->assertText(t('File to import not found.'), 'File to import not found message.');
// Try importing a .po file with overriding strings, and ensure existing
// strings are kept.
$this->importPoFile($this->getOverwritePoFile(), array(
'langcode' => 'fr',
));
// The import should have created 1 string.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.');
// Ensure string wasn't overwritten.
$search = array(
'string' => 'Montag',
'langcode' => 'fr',
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText(t('No strings available.'), 'String not overwritten by imported string.');
// This import should not have changed number of plural forms.
$locale_plurals = \Drupal::state()->get('locale.translation.plurals') ?: array();
$this->assert($locale_plurals['fr']['plurals'] == 2, 'Plural numbers untouched.');
// Try importing a .po file with overriding strings, and ensure existing
// strings are overwritten.
$this->importPoFile($this->getOverwritePoFile(), array(
'langcode' => 'fr',
'overwrite_options[not_customized]' => TRUE,
));
// The import should have updated 2 strings.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), 'The translation file was successfully imported.');
// Ensure string was overwritten.
$search = array(
'string' => 'Montag',
'langcode' => 'fr',
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), 'String overwritten by imported string.');
// This import should have changed number of plural forms.
$locale_plurals = \Drupal::state()->get('locale.translation.plurals') ?: array();
$this->assert($locale_plurals['fr']['plurals'] == 3, 'Plural numbers changed.');
// Importing a .po file and mark its strings as customized strings.
$this->importPoFile($this->getCustomPoFile(), array(
'langcode' => 'fr',
'customized' => TRUE,
));
// The import should have created 6 strings.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 6, '%update' => 0, '%delete' => 0)), 'The customized translation file was successfully imported.');
// The database should now contain 6 customized strings (two imported
// strings are not translated).
$count = db_query('SELECT COUNT(*) FROM {locales_target} WHERE customized = :custom', array(':custom' => 1))->fetchField();
$this->assertEqual($count, 6, 'Customized translations successfully imported.');
// Try importing a .po file with overriding strings, and ensure existing
// customized strings are kept.
$this->importPoFile($this->getCustomOverwritePoFile(), array(
'langcode' => 'fr',
'overwrite_options[not_customized]' => TRUE,
'overwrite_options[customized]' => FALSE,
));
// The import should have created 1 string.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), 'The customized translation file was successfully imported.');
// Ensure string wasn't overwritten.
$search = array(
'string' => 'januari',
'langcode' => 'fr',
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText(t('No strings available.'), 'Customized string not overwritten by imported string.');
// Try importing a .po file with overriding strings, and ensure existing
// customized strings are overwritten.
$this->importPoFile($this->getCustomOverwritePoFile(), array(
'langcode' => 'fr',
'overwrite_options[not_customized]' => FALSE,
'overwrite_options[customized]' => TRUE,
));
// The import should have updated 2 strings.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), 'The customized translation file was successfully imported.');
// Ensure string was overwritten.
$search = array(
'string' => 'januari',
'langcode' => 'fr',
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), 'Customized string overwritten by imported string.');
}
/**
* Test msgctxt context support.
*/
public function testLanguageContext() {
// Try importing a .po file.
$this->importPoFile($this->getPoFileWithContext(), array(
'langcode' => 'hr',
));
$this->assertIdentical(t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', 'Long month name context is working.');
$this->assertIdentical(t('May', array(), array('langcode' => 'hr')), 'Svi.', 'Default context is working.');
}
/**
* Test empty msgstr at end of .po file see #611786.
*/
public function testEmptyMsgstr() {
$langcode = 'hu';
// Try importing a .po file.
$this->importPoFile($this->getPoFileWithMsgstr(), array(
'langcode' => $langcode,
));
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.');
$this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', 'String imported and translated.');
// Try importing a .po file.
$this->importPoFile($this->getPoFileWithEmptyMsgstr(), array(
'langcode' => $langcode,
'overwrite_options[not_customized]' => TRUE,
));
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), 'The translation file was successfully imported.');
$str = "Operations";
$search = array(
'string' => $str,
'langcode' => $langcode,
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText($str, 'Search found the string as untranslated.');
}
/**
* Tests .po file import with configuration translation.
*/
public function testConfigPoFile() {
// Values for translations to assert. Config key, original string,
// translation and config property name.
$config_strings = array(
'system.maintenance' => array(
'@site is currently under maintenance. We should be back shortly. Thank you for your patience.',
'@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet.',
'message',
),
'user.role.anonymous' => array(
'Anonymous user',
'Névtelen felhasználó',
'label',
),
);
// Add custom language for testing.
$langcode = 'xx';
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $this->randomMachineName(16),
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Check for the source strings we are going to translate. Adding the
// custom language should have made the process to export configuration
// strings to interface translation executed.
$locale_storage = $this->container->get('locale.storage');
foreach ($config_strings as $config_string) {
$string = $locale_storage->findString(array('source' => $config_string[0], 'context' => '', 'type' => 'configuration'));
$this->assertTrue($string, 'Configuration strings have been created upon installation.');
}
// Import a .po file to translate.
$this->importPoFile($this->getPoFileWithConfig(), array(
'langcode' => $langcode,
));
// Translations got recorded in the interface translation system.
foreach ($config_strings as $config_string) {
$search = array(
'string' => $config_string[0],
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText($config_string[1], format_string('Translation of @string found.', array('@string' => $config_string[0])));
}
// Test that translations got recorded in the config system.
$overrides = \Drupal::service('language.config_factory_override');
foreach ($config_strings as $config_key => $config_string) {
$override = $overrides->getOverride($langcode, $config_key);
$this->assertEqual($override->get($config_string[2]), $config_string[1]);
}
}
/**
* Tests .po file import with user.settings configuration.
*/
public function testConfigtranslationImportingPoFile() {
// Set the language code.
$langcode = 'de';
// Import a .po file to translate.
$this->importPoFile($this->getPoFileWithConfigDe(), array(
'langcode' => $langcode));
// Check that the 'Anonymous' string is translated.
$config = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'user.settings');
$this->assertEqual($config->get('anonymous'), 'Anonymous German');
}
/**
* Test the translation are imported when a new language is created.
*/
public function testCreatedLanguageTranslation() {
// Import a .po file to add de language.
$this->importPoFile($this->getPoFileWithConfigDe(), array('langcode' => 'de'));
// Get the language.entity.de label and check it's been translated.
$override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'language.entity.de');
$this->assertEqual($override->get('label'), 'Deutsch');
}
/**
* Helper function: import a standalone .po file in a given language.
*
* @param string $contents
* Contents of the .po file to import.
* @param array $options
* (optional) Additional options to pass to the translation import form.
*/
public function importPoFile($contents, array $options = array()) {
$name = tempnam('temporary://', "po_") . '.po';
file_put_contents($name, $contents);
$options['files[file]'] = $name;
$this->drupalPostForm('admin/config/regional/translate/import', $options, t('Import'));
drupal_unlink($name);
}
/**
* Helper function that returns a proper .po file.
*/
public function getPoFile() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "One sheep"
msgid_plural "@count sheep"
msgstr[0] "un mouton"
msgstr[1] "@count moutons"
msgid "Monday"
msgstr "lundi"
msgid "Tuesday"
msgstr "mardi"
msgid "Wednesday"
msgstr "mercredi"
msgid "Thursday"
msgstr "jeudi"
msgid "Friday"
msgstr "vendredi"
msgid "Saturday"
msgstr "samedi"
msgid "Sunday"
msgstr "dimanche"
EOF;
}
/**
* Helper function that returns a empty .po file.
*/
public function getEmptyPoFile() {
return '';
}
/**
* Helper function that returns a bad .po file.
*/
public function getBadPoFile() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "Save configuration"
msgstr "Enregistrer la configuration"
msgid "edit"
msgstr "modifier<img SRC="javascript:alert(\'xss\');">"
msgid "delete"
msgstr "supprimer<script>alert('xss');</script>"
EOF;
}
/**
* Helper function that returns a proper .po file for testing.
*/
public function getOverwritePoFile() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"
msgid "Monday"
msgstr "Montag"
msgid "Day"
msgstr "Jour"
EOF;
}
/**
* Helper function that returns a .po file which strings will be marked
* as customized.
*/
public function getCustomPoFile() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "One dog"
msgid_plural "@count dogs"
msgstr[0] "un chien"
msgstr[1] "@count chiens"
msgid "January"
msgstr "janvier"
msgid "February"
msgstr "février"
msgid "March"
msgstr "mars"
msgid "April"
msgstr "avril"
msgid "June"
msgstr "juin"
EOF;
}
/**
* Helper function that returns a .po file for testing customized strings.
*/
public function getCustomOverwritePoFile() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "January"
msgstr "januari"
msgid "February"
msgstr "februari"
msgid "July"
msgstr "juillet"
EOF;
}
/**
* Helper function that returns a .po file with context.
*/
public function getPoFileWithContext() {
// Croatian (code hr) is one of the languages that have a different
// form for the full name and the abbreviated name for the month of May.
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"
msgctxt "Long month name"
msgid "May"
msgstr "Svibanj"
msgid "May"
msgstr "Svi."
EOF;
}
/**
* Helper function that returns a .po file with an empty last item.
*/
public function getPoFileWithEmptyMsgstr() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "Operations"
msgstr ""
EOF;
}
/**
* Helper function that returns a .po file with an empty last item.
*/
public function getPoFileWithMsgstr() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "Operations"
msgstr "Műveletek"
msgid "Will not appear in Drupal core, so we can ensure the test passes"
msgstr ""
EOF;
}
/**
* Helper function that returns a .po file with configuration translations.
*/
public function getPoFileWithConfig() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
msgstr "@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet."
msgid "Anonymous user"
msgstr "Névtelen felhasználó"
EOF;
}
/**
* Helper function that returns a .po file with configuration translations.
*/
public function getPoFileWithConfigDe() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "Anonymous"
msgstr "Anonymous German"
msgid "German"
msgstr "Deutsch"
EOF;
}
}

View file

@ -0,0 +1,150 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleJavascriptTranslationTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\Language\LanguageInterface;
use Drupal\simpletest\WebTestBase;
use Drupal\Component\Utility\SafeMarkup;
/**
* Tests parsing js files for translatable strings.
*
* @group locale
*/
class LocaleJavascriptTranslationTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
public function testFileParsing() {
$filename = drupal_get_path('module', 'locale') . '/tests/locale_test.js';
// Parse the file to look for source strings.
_locale_parse_js_file($filename);
// Get all of the source strings that were found.
$strings = $this->container
->get('locale.storage')
->getStrings(array(
'type' => 'javascript',
'name' => $filename,
));
$source_strings = array();
foreach ($strings as $string) {
$source_strings[$string->source] = $string->context;
}
$etx = LOCALE_PLURAL_DELIMITER;
// List of all strings that should be in the file.
$test_strings = array(
'Standard Call t' => '',
'Whitespace Call t' => '',
'Single Quote t' => '',
"Single Quote \\'Escaped\\' t" => '',
'Single Quote Concat strings t' => '',
'Double Quote t' => '',
"Double Quote \\\"Escaped\\\" t" => '',
'Double Quote Concat strings t' => '',
'Context !key Args t' => 'Context string',
'Context Unquoted t' => 'Context string unquoted',
'Context Single Quoted t' => 'Context string single quoted',
'Context Double Quoted t' => 'Context string double quoted',
"Standard Call plural{$etx}Standard Call @count plural" => '',
"Whitespace Call plural{$etx}Whitespace Call @count plural" => '',
"Single Quote plural{$etx}Single Quote @count plural" => '',
"Single Quote \\'Escaped\\' plural{$etx}Single Quote \\'Escaped\\' @count plural" => '',
"Double Quote plural{$etx}Double Quote @count plural" => '',
"Double Quote \\\"Escaped\\\" plural{$etx}Double Quote \\\"Escaped\\\" @count plural" => '',
"Context !key Args plural{$etx}Context !key Args @count plural" => 'Context string',
"Context Unquoted plural{$etx}Context Unquoted @count plural" => 'Context string unquoted',
"Context Single Quoted plural{$etx}Context Single Quoted @count plural" => 'Context string single quoted',
"Context Double Quoted plural{$etx}Context Double Quoted @count plural" => 'Context string double quoted',
);
// Assert that all strings were found properly.
foreach ($test_strings as $str => $context) {
$args = array('%source' => $str, '%context' => $context);
// Make sure that the string was found in the file.
$this->assertTrue(isset($source_strings[$str]), SafeMarkup::format('Found source string: %source', $args));
// Make sure that the proper context was matched.
$message = $context ? SafeMarkup::format('Context for %source is %context', $args) : SafeMarkup::format('Context for %source is blank', $args);
$this->assertTrue(isset($source_strings[$str]) && $source_strings[$str] === $context, $message);
}
$this->assertEqual(count($source_strings), count($test_strings), 'Found correct number of source strings.');
}
/**
* Assert translations JS is added before drupal.js, because it depends on it.
*/
public function testLocaleTranslationJsDependencies() {
// User to add and remove language.
$admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface'));
// Add custom language.
$this->drupalLogin($admin_user);
// Code for the language.
$langcode = 'es';
// The English name for the language.
$name = $this->randomMachineName(16);
// The domain prefix.
$prefix = $langcode;
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Set path prefix.
$edit = array("prefix[$langcode]" => $prefix);
$this->drupalPostForm('admin/config/regional/language/detection/url', $edit, t('Save configuration'));
// This forces locale.admin.js string sources to be imported, which contains
// the next translation.
$this->drupalGet($prefix . '/admin/config/regional/translate');
// Translate a string in locale.admin.js to our new language.
$strings = \Drupal::service('locale.storage')
->getStrings(array(
'source' => 'Show description',
'type' => 'javascript',
'name' => 'core/modules/locale/locale.admin.js',
));
$string = $strings[0];
$this->drupalPostForm(NULL, ['string' => 'Show description'], t('Filter'));
$edit = ['strings[' . $string->lid . '][translations][0]' => $this->randomString(16)];
$this->drupalPostForm(NULL, $edit, t('Save translations'));
// Calculate the filename of the JS including the translations.
$js_translation_files = \Drupal::state()->get('locale.translation.javascript');
$js_filename = $prefix . '_' . $js_translation_files[$prefix] . '.js';
// Assert translations JS is included before drupal.js.
$this->assertTrue(strpos($this->content, $js_filename) < strpos($this->content, 'core/misc/drupal.js'), 'Translations are included before Drupal.t.');
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleLibraryAlterTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\simpletest\WebTestBase;
/**
* Tests localization of the JavaScript libraries.
*
* Currently, only the jQuery datepicker is localized using Drupal translations.
*
* @group locale
*/
class LocaleLibraryAlterTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
/**
* Verifies that the datepicker can be localized.
*
* @see locale_library_alter()
*/
public function testLibraryAlter() {
$assets = new AttachedAssets();
$assets->setLibraries(['core/jquery.ui.datepicker']);
$js_assets = $this->container->get('asset.resolver')->getJsAssets($assets, FALSE)[1];
$this->assertTrue(array_key_exists('core/modules/locale/locale.datepicker.js', $js_assets), 'locale.datepicker.js added to scripts.');
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleLocaleLookupTest.
*/
namespace Drupal\locale\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
/**
* Tests LocaleLookup.
*
* @group locale
*/
class LocaleLocaleLookupTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale', 'locale_test');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Change the language default object to different values.
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->config('system.site')->set('default_langcode', 'fr')->save();
$this->drupalLogin($this->rootUser);
}
/**
* Tests that there are no circular dependencies.
*/
public function testCircularDependency() {
// Ensure that we can enable early_translation_test on a non-english site.
$this->drupalPostForm('admin/modules', array('modules[Testing][early_translation_test][enable]' => TRUE), t('Save configuration'));
$this->assertResponse(200);
}
/**
* Test language fallback defaults.
*/
public function testLanguageFallbackDefaults() {
$this->drupalGet('');
// Ensure state of fallback languages persisted by
// locale_test_language_fallback_candidates_locale_lookup_alter() is empty.
$this->assertEqual(\Drupal::state()->get('locale.test_language_fallback_candidates_locale_lookup_alter_candidates'), array());
// Make sure there is enough information provided for alter hooks.
$context = \Drupal::state()->get('locale.test_language_fallback_candidates_locale_lookup_alter_context');
$this->assertEqual($context['langcode'], 'fr');
$this->assertEqual($context['operation'], 'locale_lookup');
}
}

View file

@ -0,0 +1,155 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocalePathTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
/**
* Tests you can configure a language for individual URL aliases.
*
* @group locale
*/
class LocalePathTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'locale', 'path', 'views');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
$this->config('system.site')->set('page.front', '/node')->save();
}
/**
* Test if a language can be associated with a path alias.
*/
public function testPathLanguageConfiguration() {
// User to add and remove language.
$admin_user = $this->drupalCreateUser(array('administer languages', 'create page content', 'administer url aliases', 'create url aliases', 'access administration pages', 'access content overview'));
// Add custom language.
$this->drupalLogin($admin_user);
// Code for the language.
$langcode = 'xx';
// The English name for the language.
$name = $this->randomMachineName(16);
// The domain prefix.
$prefix = $langcode;
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Set path prefix.
$edit = array("prefix[$langcode]" => $prefix);
$this->drupalPostForm('admin/config/regional/language/detection/url', $edit, t('Save configuration'));
// Check that the "xx" front page is readily available because path prefix
// negotiation is pre-configured.
$this->drupalGet($prefix);
$this->assertText(t('Welcome to Drupal'), 'The "xx" front page is readibly available.');
// Create a node.
$node = $this->drupalCreateNode(array('type' => 'page'));
// Create a path alias in default language (English).
$path = 'admin/config/search/path/add';
$english_path = $this->randomMachineName(8);
$edit = array(
'source' => '/node/' . $node->id(),
'alias' => '/' . $english_path,
'langcode' => 'en',
);
$this->drupalPostForm($path, $edit, t('Save'));
// Create a path alias in new custom language.
$custom_language_path = $this->randomMachineName(8);
$edit = array(
'source' => '/node/' . $node->id(),
'alias' => '/' . $custom_language_path,
'langcode' => $langcode,
);
$this->drupalPostForm($path, $edit, t('Save'));
// Confirm English language path alias works.
$this->drupalGet($english_path);
$this->assertText($node->label(), 'English alias works.');
// Confirm custom language path alias works.
$this->drupalGet($prefix . '/' . $custom_language_path);
$this->assertText($node->label(), 'Custom language alias works.');
// Create a custom path.
$custom_path = $this->randomMachineName(8);
// Check priority of language for alias by source path.
$edit = array(
'source' => '/node/' . $node->id(),
'alias' => '/' . $custom_path,
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
);
$this->container->get('path.alias_storage')->save($edit['source'], $edit['alias'], $edit['langcode']);
$lookup_path = $this->container->get('path.alias_manager')->getAliasByPath('/node/' . $node->id(), 'en');
$this->assertEqual('/' . $english_path, $lookup_path, 'English language alias has priority.');
// Same check for language 'xx'.
$lookup_path = $this->container->get('path.alias_manager')->getAliasByPath('/node/' . $node->id(), $prefix);
$this->assertEqual('/' . $custom_language_path, $lookup_path, 'Custom language alias has priority.');
$this->container->get('path.alias_storage')->delete($edit);
// Create language nodes to check priority of aliases.
$first_node = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1, 'langcode' => 'en'));
$second_node = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1, 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED));
// Assign a custom path alias to the first node with the English language.
$edit = array(
'source' => '/node/' . $first_node->id(),
'alias' => '/' . $custom_path,
'langcode' => $first_node->language()->getId(),
);
$this->container->get('path.alias_storage')->save($edit['source'], $edit['alias'], $edit['langcode']);
// Assign a custom path alias to second node with
// LanguageInterface::LANGCODE_NOT_SPECIFIED.
$edit = array(
'source' => '/node/' . $second_node->id(),
'alias' => '/' . $custom_path,
'langcode' => $second_node->language()->getId(),
);
$this->container->get('path.alias_storage')->save($edit['source'], $edit['alias'], $edit['langcode']);
// Test that both node titles link to our path alias.
$this->drupalGet('admin/content');
$custom_path_url = Url::fromUserInput('/' . $custom_path)->toString();
$elements = $this->xpath('//a[@href=:href and normalize-space(text())=:title]', array(':href' => $custom_path_url, ':title' => $first_node->label()));
$this->assertTrue(!empty($elements), 'First node links to the path alias.');
$elements = $this->xpath('//a[@href=:href and normalize-space(text())=:title]', array(':href' => $custom_path_url, ':title' => $second_node->label()));
$this->assertTrue(!empty($elements), 'Second node links to the path alias.');
// Confirm that the custom path leads to the first node.
$this->drupalGet($custom_path);
$this->assertText($first_node->label(), 'Custom alias returns first node.');
// Confirm that the custom path with prefix leads to the second node.
$this->drupalGet($prefix . '/' . $custom_path);
$this->assertText($second_node->label(), 'Custom alias with prefix returns second node.');
}
}

View file

@ -0,0 +1,370 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocalePluralFormatTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests plural handling for various languages.
*
* @group locale
*/
class LocalePluralFormatTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
$this->drupalLogin($admin_user);
}
/**
* Tests locale_get_plural() and \Drupal::translation()->formatPlural()
* functionality.
*/
public function testGetPluralFormat() {
// Import some .po files with formulas to set up the environment.
// These will also add the languages to the system.
$this->importPoFile($this->getPoFileWithSimplePlural(), array(
'langcode' => 'fr',
));
$this->importPoFile($this->getPoFileWithComplexPlural(), array(
'langcode' => 'hr',
));
// Attempt to import some broken .po files as well to prove that these
// will not overwrite the proper plural formula imported above.
$this->importPoFile($this->getPoFileWithMissingPlural(), array(
'langcode' => 'fr',
'overwrite_options[not_customized]' => TRUE,
));
$this->importPoFile($this->getPoFileWithBrokenPlural(), array(
'langcode' => 'hr',
'overwrite_options[not_customized]' => TRUE,
));
// Reset static caches from locale_get_plural() to ensure we get fresh data.
drupal_static_reset('locale_get_plural');
drupal_static_reset('locale_get_plural:plurals');
drupal_static_reset('locale');
// Expected plural translation strings for each plural index.
$plural_strings = array(
// English is not imported in this case, so we assume built-in text
// and formulas.
'en' => array(
0 => '1 hour',
1 => '@count hours',
),
'fr' => array(
0 => '@count heure',
1 => '@count heures',
),
'hr' => array(
0 => '@count sat',
1 => '@count sata',
2 => '@count sati',
),
// Hungarian is not imported, so it should assume the same text as
// English, but it will always pick the plural form as per the built-in
// logic, so only index -1 is relevant with the plural value.
'hu' => array(
0 => '1 hour',
-1 => '@count hours',
),
);
// Expected plural indexes precomputed base on the plural formulas with
// given $count value.
$plural_tests = array(
'en' => array(
1 => 0,
0 => 1,
5 => 1,
123 => 1,
235 => 1,
),
'fr' => array(
1 => 0,
0 => 0,
5 => 1,
123 => 1,
235 => 1,
),
'hr' => array(
1 => 0,
21 => 0,
0 => 2,
2 => 1,
8 => 2,
123 => 1,
235 => 2,
),
'hu' => array(
1 => -1,
21 => -1,
0 => -1,
),
);
foreach ($plural_tests as $langcode => $tests) {
foreach ($tests as $count => $expected_plural_index) {
// Assert that the we get the right plural index.
$this->assertIdentical(locale_get_plural($count, $langcode), $expected_plural_index, 'Computed plural index for ' . $langcode . ' for count ' . $count . ' is ' . $expected_plural_index);
// Assert that the we get the right translation for that. Change the
// expected index as per the logic for translation lookups.
$expected_plural_index = ($count == 1) ? 0 : $expected_plural_index;
$expected_plural_string = str_replace('@count', $count, $plural_strings[$langcode][$expected_plural_index]);
$this->assertIdentical(\Drupal::translation()->formatPlural($count, '1 hour', '@count hours', array(), array('langcode' => $langcode)), $expected_plural_string, 'Plural translation of 1 hours / @count hours for count ' . $count . ' in ' . $langcode . ' is ' . $expected_plural_string);
// DO NOT use translation to pass into formatPluralTranslated() this
// way. It is designed to be used with *already* translated text like
// settings from configuration. We use PHP translation here just because
// we have the expected result data in that format.
$this->assertIdentical(\Drupal::translation()->formatPluralTranslated($count, \Drupal::translation()->translate('1 hour' . LOCALE_PLURAL_DELIMITER . '@count hours', array(), array('langcode' => $langcode)), array(), array('langcode' => $langcode)), $expected_plural_string, 'Translated plural lookup of 1 hours / @count hours for count ' . $count . ' in ' . $langcode . ' is ' . $expected_plural_string);
}
}
}
/**
* Tests plural editing and export functionality.
*/
public function testPluralEditExport() {
// Import some .po files with formulas to set up the environment.
// These will also add the languages to the system.
$this->importPoFile($this->getPoFileWithSimplePlural(), array(
'langcode' => 'fr',
));
$this->importPoFile($this->getPoFileWithComplexPlural(), array(
'langcode' => 'hr',
));
// Get the French translations.
$this->drupalPostForm('admin/config/regional/translate/export', array(
'langcode' => 'fr',
), t('Export'));
// Ensure we have a translation file.
$this->assertRaw('# French translation of Drupal', 'Exported French translation file.');
// Ensure our imported translations exist in the file.
$this->assertRaw("msgid \"Monday\"\nmsgstr \"lundi\"", 'French translations present in exported file.');
// Check for plural export specifically.
$this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count heure\"\nmsgstr[1] \"@count heures\"", 'Plural translations exported properly.');
// Get the Croatian translations.
$this->drupalPostForm('admin/config/regional/translate/export', array(
'langcode' => 'hr',
), t('Export'));
// Ensure we have a translation file.
$this->assertRaw('# Croatian translation of Drupal', 'Exported Croatian translation file.');
// Ensure our imported translations exist in the file.
$this->assertRaw("msgid \"Monday\"\nmsgstr \"Ponedjeljak\"", 'Croatian translations present in exported file.');
// Check for plural export specifically.
$this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count sat\"\nmsgstr[1] \"@count sata\"\nmsgstr[2] \"@count sati\"", 'Plural translations exported properly.');
// Check if the source appears on the translation page.
$this->drupalGet('admin/config/regional/translate');
$this->assertText("1 hour");
$this->assertText("@count hours");
// Look up editing page for this plural string and check fields.
$path = 'admin/config/regional/translate/';
$search = array(
'langcode' => 'hr',
);
$this->drupalPostForm($path, $search, t('Filter'));
// Labels for plural editing elements.
$this->assertText('Singular form');
$this->assertText('First plural form');
$this->assertText('2. plural form');
$this->assertNoText('3. plural form');
// Plural values for langcode hr.
$this->assertText('@count sat');
$this->assertText('@count sata');
$this->assertText('@count sati');
// Edit langcode hr translations and see if that took effect.
$lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", array(':source' => "1 hour" . LOCALE_PLURAL_DELIMITER . "@count hours"))->fetchField();
$edit = array(
"strings[$lid][translations][1]" => '@count sata edited',
);
$this->drupalPostForm($path, $edit, t('Save translations'));
$search = array(
'langcode' => 'fr',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
// Plural values for the langcode fr.
$this->assertText('@count heure');
$this->assertText('@count heures');
$this->assertNoText('2. plural form');
// Edit langcode fr translations and see if that took effect.
$edit = array(
"strings[$lid][translations][0]" => '@count heure edited',
);
$this->drupalPostForm($path, $edit, t('Save translations'));
// Inject a plural source string to the database. We need to use a specific
// langcode here because the language will be English by default and will
// not save our source string for performance optimization if we do not ask
// specifically for a language.
\Drupal::translation()->formatPlural(1, '1 day', '@count days', array(), array('langcode' => 'fr'));
$lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", array(':source' => "1 day" . LOCALE_PLURAL_DELIMITER . "@count days"))->fetchField();
// Look up editing page for this plural string and check fields.
$search = array(
'string' => '1 day',
'langcode' => 'fr',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
// Save complete translations for the string in langcode fr.
$edit = array(
"strings[$lid][translations][0]" => '1 jour',
"strings[$lid][translations][1]" => '@count jours',
);
$this->drupalPostForm($path, $edit, t('Save translations'));
// Save complete translations for the string in langcode hr.
$search = array(
'string' => '1 day',
'langcode' => 'hr',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$edit = array(
"strings[$lid][translations][0]" => '@count dan',
"strings[$lid][translations][1]" => '@count dana',
"strings[$lid][translations][2]" => '@count dana',
);
$this->drupalPostForm($path, $edit, t('Save translations'));
// Get the French translations.
$this->drupalPostForm('admin/config/regional/translate/export', array(
'langcode' => 'fr',
), t('Export'));
// Check for plural export specifically.
$this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count heure edited\"\nmsgstr[1] \"@count heures\"", 'Edited French plural translations for hours exported properly.');
$this->assertRaw("msgid \"1 day\"\nmsgid_plural \"@count days\"\nmsgstr[0] \"1 jour\"\nmsgstr[1] \"@count jours\"", 'Added French plural translations for days exported properly.');
// Get the Croatian translations.
$this->drupalPostForm('admin/config/regional/translate/export', array(
'langcode' => 'hr',
), t('Export'));
// Check for plural export specifically.
$this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count sat\"\nmsgstr[1] \"@count sata edited\"\nmsgstr[2] \"@count sati\"", 'Edited Croatian plural translations exported properly.');
$this->assertRaw("msgid \"1 day\"\nmsgid_plural \"@count days\"\nmsgstr[0] \"@count dan\"\nmsgstr[1] \"@count dana\"\nmsgstr[2] \"@count dana\"", 'Added Croatian plural translations exported properly.');
}
/**
* Imports a standalone .po file in a given language.
*
* @param string $contents
* Contents of the .po file to import.
* @param array $options
* Additional options to pass to the translation import form.
*/
public function importPoFile($contents, array $options = array()) {
$name = tempnam('temporary://', "po_") . '.po';
file_put_contents($name, $contents);
$options['files[file]'] = $name;
$this->drupalPostForm('admin/config/regional/translate/import', $options, t('Import'));
drupal_unlink($name);
}
/**
* Returns a .po file with a simple plural formula.
*/
public function getPoFileWithSimplePlural() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "1 hour"
msgid_plural "@count hours"
msgstr[0] "@count heure"
msgstr[1] "@count heures"
msgid "Monday"
msgstr "lundi"
EOF;
}
/**
* Returns a .po file with a complex plural formula.
*/
public function getPoFileWithComplexPlural() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"
msgid "1 hour"
msgid_plural "@count hours"
msgstr[0] "@count sat"
msgstr[1] "@count sata"
msgstr[2] "@count sati"
msgid "Monday"
msgstr "Ponedjeljak"
EOF;
}
/**
* Returns a .po file with a missing plural formula.
*/
public function getPoFileWithMissingPlural() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
msgid "Monday"
msgstr "lundi"
EOF;
}
/**
* Returns a .po file with a broken plural formula.
*/
public function getPoFileWithBrokenPlural() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: broken, will not parse\\n"
msgid "Monday"
msgstr "Ponedjeljak"
EOF;
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleStringTest.
*/
namespace Drupal\locale\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
/**
* Tests the locale string storage, string objects and data API.
*
* @group locale
*/
class LocaleStringTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
/**
* The locale storage.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected $storage;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Add a default locale storage for all these tests.
$this->storage = $this->container->get('locale.storage');
// Create two languages: Spanish and German.
foreach (array('es', 'de') as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
}
/**
* Test CRUD API.
*/
public function testStringCRUDAPI() {
// Create source string.
$source = $this->buildSourceString();
$source->save();
$this->assertTrue($source->lid, format_string('Successfully created string %string', array('%string' => $source->source)));
// Load strings by lid and source.
$string1 = $this->storage->findString(array('lid' => $source->lid));
$this->assertEqual($source, $string1, 'Successfully retrieved string by identifier.');
$string2 = $this->storage->findString(array('source' => $source->source, 'context' => $source->context));
$this->assertEqual($source, $string2, 'Successfully retrieved string by source and context.');
$string3 = $this->storage->findString(array('source' => $source->source, 'context' => ''));
$this->assertFalse($string3, 'Cannot retrieve string with wrong context.');
// Check version handling and updating.
$this->assertEqual($source->version, 'none', 'String originally created without version.');
$string = $this->storage->findTranslation(array('lid' => $source->lid));
$this->assertEqual($string->version, \Drupal::VERSION, 'Checked and updated string version to Drupal version.');
// Create translation and find it by lid and source.
$langcode = 'es';
$translation = $this->createTranslation($source, $langcode);
$this->assertEqual($translation->customized, LOCALE_NOT_CUSTOMIZED, 'Translation created as not customized by default.');
$string1 = $this->storage->findTranslation(array('language' => $langcode, 'lid' => $source->lid));
$this->assertEqual($string1->translation, $translation->translation, 'Successfully loaded translation by string identifier.');
$string2 = $this->storage->findTranslation(array('language' => $langcode, 'source' => $source->source, 'context' => $source->context));
$this->assertEqual($string2->translation, $translation->translation, 'Successfully loaded translation by source and context.');
$translation
->setCustomized()
->save();
$translation = $this->storage->findTranslation(array('language' => $langcode, 'lid' => $source->lid));
$this->assertEqual($translation->customized, LOCALE_CUSTOMIZED, 'Translation successfully marked as customized.');
// Delete translation.
$translation->delete();
$deleted = $this->storage->findTranslation(array('language' => $langcode, 'lid' => $source->lid));
$this->assertFalse(isset($deleted->translation), 'Successfully deleted translation string.');
// Create some translations and then delete string and all of its
// translations.
$lid = $source->lid;
$this->createAllTranslations($source);
$search = $this->storage->getTranslations(array('lid' => $source->lid));
$this->assertEqual(count($search), 3, 'Created and retrieved all translations for our source string.');
$source->delete();
$string = $this->storage->findString(array('lid' => $lid));
$this->assertFalse($string, 'Successfully deleted source string.');
$deleted = $search = $this->storage->getTranslations(array('lid' => $lid));
$this->assertFalse($deleted, 'Successfully deleted all translation strings.');
// Tests that locations of different types and arbitrary lengths can be
// added to a source string. Too long locations will be cut off.
$source_string = $this->buildSourceString();
$source_string->addLocation('javascript', $this->randomString(8));
$source_string->addLocation('configuration', $this->randomString(50));
$source_string->addLocation('code', $this->randomString(100));
$source_string->addLocation('path', $location = $this->randomString(300));
$source_string->save();
$rows = db_query('SELECT * FROM {locales_location} WHERE sid = :sid', array(':sid' => $source_string->lid))->fetchAllAssoc('type');
$this->assertEqual(count($rows), 4, '4 source locations have been persisted.');
$this->assertEqual($rows['path']->name, substr($location, 0, 255), 'Too long location has been limited to 255 characters.');
}
/**
* Test Search API loading multiple objects.
*/
public function testStringSearchAPI() {
$language_count = 3;
// Strings 1 and 2 will have some common prefix.
// Source 1 will have all translations, not customized.
// Source 2 will have all translations, customized.
// Source 3 will have no translations.
$prefix = $this->randomMachineName(100);
$source1 = $this->buildSourceString(array('source' => $prefix . $this->randomMachineName(100)))->save();
$source2 = $this->buildSourceString(array('source' => $prefix . $this->randomMachineName(100)))->save();
$source3 = $this->buildSourceString()->save();
// Load all source strings.
$strings = $this->storage->getStrings(array());
$this->assertEqual(count($strings), 3, 'Found 3 source strings in the database.');
// Load all source strings matching a given string.
$filter_options['filters'] = array('source' => $prefix);
$strings = $this->storage->getStrings(array(), $filter_options);
$this->assertEqual(count($strings), 2, 'Found 2 strings using some string filter.');
// Not customized translations.
$translate1 = $this->createAllTranslations($source1);
// Customized translations.
$this->createAllTranslations($source2, array('customized' => LOCALE_CUSTOMIZED));
// Try quick search function with different field combinations.
$langcode = 'es';
$found = $this->storage->findTranslation(array('language' => $langcode, 'source' => $source1->source, 'context' => $source1->context));
$this->assertTrue($found && isset($found->language) && isset($found->translation) && !$found->isNew(), 'Translation found searching by source and context.');
$this->assertEqual($found->translation, $translate1[$langcode]->translation, 'Found the right translation.');
// Now try a translation not found.
$found = $this->storage->findTranslation(array('language' => $langcode, 'source' => $source3->source, 'context' => $source3->context));
$this->assertTrue($found && $found->lid == $source3->lid && !isset($found->translation) && $found->isNew(), 'Translation not found but source string found.');
// Load all translations. For next queries we'll be loading only translated
// strings.
$translations = $this->storage->getTranslations(array('translated' => TRUE));
$this->assertEqual(count($translations), 2 * $language_count, 'Created and retrieved all translations for source strings.');
// Load all customized translations.
$translations = $this->storage->getTranslations(array('customized' => LOCALE_CUSTOMIZED, 'translated' => TRUE));
$this->assertEqual(count($translations), $language_count, 'Retrieved all customized translations for source strings.');
// Load all Spanish customized translations.
$translations = $this->storage->getTranslations(array('language' => 'es', 'customized' => LOCALE_CUSTOMIZED, 'translated' => TRUE));
$this->assertEqual(count($translations), 1, 'Found only Spanish and customized translations.');
// Load all source strings without translation (1).
$translations = $this->storage->getStrings(array('translated' => FALSE));
$this->assertEqual(count($translations), 1, 'Found 1 source string without translations.');
// Load Spanish translations using string filter.
$filter_options['filters'] = array('source' => $prefix);
$translations = $this->storage->getTranslations(array('language' => 'es'), $filter_options);
$this->assertEqual(count($translations), 2, 'Found 2 translations using some string filter.');
}
/**
* Creates random source string object.
*
* @return \Drupal\locale\StringInterface
* A locale string.
*/
public function buildSourceString($values = array()) {
return $this->storage->createString($values += array(
'source' => $this->randomMachineName(100),
'context' => $this->randomMachineName(20),
));
}
/**
* Creates translations for source string and all languages.
*/
public function createAllTranslations($source, $values = array()) {
$list = array();
/* @var $language_manager \Drupal\Core\Language\LanguageManagerInterface */
$language_manager = $this->container->get('language_manager');
foreach ($language_manager->getLanguages() as $language) {
$list[$language->getId()] = $this->createTranslation($source, $language->getId(), $values);
}
return $list;
}
/**
* Creates single translation for source string.
*/
public function createTranslation($source, $langcode, $values = array()) {
return $this->storage->createTranslation($values + array(
'lid' => $source->lid,
'language' => $langcode,
'translation' => $this->randomMachineName(100),
))->save();
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleTranslateStringTourTest.
*/
namespace Drupal\locale\Tests;
use Drupal\tour\Tests\TourTestBase;
/**
* Tests the Translate Interface tour.
*
* @group locale
*/
class LocaleTranslateStringTourTest extends TourTestBase {
/**
* An admin user with administrative permissions to translate.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale', 'tour');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(array('translate interface', 'access tour', 'administer languages'));
$this->drupalLogin($this->adminUser);
}
/**
* Tests locale tour tip availability.
*/
public function testTranslateStringTourTips() {
// Add another language so there are no missing form items.
$edit = array();
$edit['predefined_langcode'] = 'es';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$this->drupalGet('admin/config/regional/translate');
$this->assertTourTips();
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleTranslatedSchemaDefinitionTest.
*/
namespace Drupal\locale\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
/**
* Adds and configures languages to check field schema definition.
*
* @group locale
*/
class LocaleTranslatedSchemaDefinitionTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'locale', 'node');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->config('system.site')->set('default_langcode', 'fr')->save();
// Make sure new entity type definitions are processed.
\Drupal::service('entity.definition_update_manager')->applyUpdates();
// Clear all caches so that the base field definition, its cache in the
// entity manager, the t() cache, etc. are all cleared.
drupal_flush_all_caches();
}
/**
* Tests that translated field descriptions do not affect the update system.
*/
function testTranslatedSchemaDefinition() {
/** @var \Drupal\locale\StringDatabaseStorage $stringStorage */
$stringStorage = \Drupal::service('locale.storage');
$source = $stringStorage->createString(array(
'source' => 'The node ID.',
))->save();
$stringStorage->createTranslation(array(
'lid' => $source->lid,
'language' => 'fr',
'translation' => 'Translated node ID',
))->save();
// Ensure that the field is translated when access through the API.
$this->assertEqual('Translated node ID', \Drupal::entityManager()->getBaseFieldDefinitions('node')['nid']->getDescription());
// Assert there are no updates.
$this->assertFalse(\Drupal::service('entity.definition_update_manager')->needsUpdates());
}
/**
* Tests that translations do not affect the update system.
*/
function testTranslatedUpdate() {
// Visit the update page to collect any strings that may be translatable.
$user = $this->drupalCreateUser(array('administer software updates'));
$this->drupalLogin($user);
$update_url = $GLOBALS['base_url'] . '/update.php';
$this->drupalGet($update_url, array('external' => TRUE));
/** @var \Drupal\locale\StringDatabaseStorage $stringStorage */
$stringStorage = \Drupal::service('locale.storage');
$sources = $stringStorage->getStrings();
// Translate all source strings found.
foreach ($sources as $source) {
$stringStorage->createTranslation(array(
'lid' => $source->lid,
'language' => 'fr',
'translation' => $this->randomMachineName(100),
))->save();
}
// Ensure that there are no updates just due to translations. Check for
// markup and a link instead of specific text because text may be
// translated.
$this->drupalGet($update_url . '/selection', array('external' => TRUE));
$this->assertRaw('messages--status', 'No pending updates.');
$this->assertNoLinkByHref('fr/update.php/run', 'No link to run updates.');
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleTranslationProjectsTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\KernelTestBase;
/**
* Tests locale translation project handling.
*
* @group locale
*/
class LocaleTranslationProjectsTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['locale'];
/**
* The module handler used in this test.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The locale project storage used in this test.
*
* @var \Drupal\locale\LocaleProjectStorageInterface
*/
protected $projectStorage;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->moduleHandler = $this->container->get('module_handler');
$this->projectStorage = $this->container->get('locale.project');
}
/**
* Tests locale_translation_clear_cache_projects().
*/
public function testLocaleTranslationClearCacheProjects() {
$this->moduleHandler->loadInclude('locale', 'inc', 'locale.translation');
$expected = [];
$this->assertIdentical($expected, locale_translation_get_projects());
$this->projectStorage->set('foo', []);
$expected['foo'] = new \stdClass();
$this->assertEqual($expected, locale_translation_get_projects());
$this->projectStorage->set('bar', []);
locale_translation_clear_cache_projects();
$expected['bar'] = new \stdClass();
$this->assertEqual($expected, locale_translation_get_projects());
}
}

View file

@ -0,0 +1,546 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleTranslationUiTest.
*/
namespace Drupal\locale\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Component\Utility\SafeMarkup;
/**
* Adds a new locale and translates its name. Checks the validation of
* translation strings and search results.
*
* @group locale
*/
class LocaleTranslationUiTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
/**
* Enable interface translation to English.
*/
public function testEnglishTranslation() {
$admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
$this->drupalLogin($admin_user);
$this->drupalPostForm('admin/config/regional/language/edit/en', array('locale_translate_english' => TRUE), t('Save language'));
$this->assertLinkByHref('/admin/config/regional/translate?langcode=en', 0, 'Enabled interface translation to English.');
}
/**
* Adds a language and tests string translation by users with the appropriate permissions.
*/
public function testStringTranslation() {
// User to add and remove language.
$admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
// User to translate and delete string.
$translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
// Code for the language.
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// This will be the translation of $name.
$translation = $this->randomMachineName(16);
$translation_to_en = $this->randomMachineName(16);
// Add custom language.
$this->drupalLogin($admin_user);
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Add string.
t($name, array(), array('langcode' => $langcode));
// Reset locale cache.
$this->container->get('string_translation')->reset();
$this->assertRaw('"edit-languages-' . $langcode . '-weight"', 'Language code found.');
$this->assertText(t($name), 'Test language added.');
$this->drupalLogout();
// Search for the name and translate it.
$this->drupalLogin($translate_user);
$search = array(
'string' => $name,
'langcode' => $langcode,
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText($name, 'Search found the string as untranslated.');
// No t() here, it's surely not translated yet.
$this->assertText($name, 'name found on edit screen.');
$this->assertNoOption('edit-langcode', 'en', 'No way to translate the string to English.');
$this->drupalLogout();
$this->drupalLogin($admin_user);
$this->drupalPostForm('admin/config/regional/language/edit/en', array('locale_translate_english' => TRUE), t('Save language'));
$this->drupalLogout();
$this->drupalLogin($translate_user);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText($name, 'Search found the string as untranslated.');
// Assume this is the only result, given the random name.
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $translation,
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
$this->assertText(t('The strings have been saved.'), 'The strings have been saved.');
$url_bits = explode('?', $this->getUrl());
$this->assertEqual($url_bits[0], \Drupal::url('locale.translate_page', array(), array('absolute' => TRUE)), 'Correct page redirection.');
$search = array(
'string' => $name,
'langcode' => $langcode,
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertRaw($translation, 'Non-English translation properly saved.');
$search = array(
'string' => $name,
'langcode' => 'en',
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $translation_to_en,
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
$search = array(
'string' => $name,
'langcode' => 'en',
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertRaw($translation_to_en, 'English translation properly saved.');
$this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, 't() works for non-English.');
// Refresh the locale() cache to get fresh data from t() below. We are in
// the same HTTP request and therefore t() is not refreshed by saving the
// translation above.
$this->container->get('string_translation')->reset();
// Now we should get the proper fresh translation from t().
$this->assertTrue($name != $translation_to_en && t($name, array(), array('langcode' => 'en')) == $translation_to_en, 't() works for English.');
$this->assertTrue(t($name, array(), array('langcode' => LanguageInterface::LANGCODE_SYSTEM)) == $name, 't() works for LanguageInterface::LANGCODE_SYSTEM.');
$search = array(
'string' => $name,
'langcode' => 'en',
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText(t('No strings available.'), 'String is translated.');
// Test invalidation of 'rendered' cache tag after string translation.
$this->drupalLogout();
$this->drupalGet('xx/user/login');
$this->assertText('Enter the password that accompanies your username.');
$this->drupalLogin($translate_user);
$search = array(
'string' => 'accompanies your username',
'langcode' => $langcode,
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => 'Please enter your Llama username.',
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
$this->drupalLogout();
$this->drupalGet('xx/user/login');
$this->assertText('Please enter your Llama username.');
// Delete the language.
$this->drupalLogin($admin_user);
$path = 'admin/config/regional/language/delete/' . $langcode;
// This a confirm form, we do not need any fields changed.
$this->drupalPostForm($path, array(), t('Delete'));
// We need raw here because %language and %langcode will add HTML.
$t_args = array('%language' => $name, '%langcode' => $langcode);
$this->assertRaw(t('The %language (%langcode) language has been removed.', $t_args), 'The test language has been removed.');
// Reload to remove $name.
$this->drupalGet($path);
// Verify that language is no longer found.
$this->assertResponse(404, 'Language no longer found.');
$this->drupalLogout();
// Delete the string.
$this->drupalLogin($translate_user);
$search = array(
'string' => $name,
'langcode' => 'en',
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
// Assume this is the only result, given the random name.
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => '',
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
$this->assertRaw($name, 'The strings have been saved.');
$this->drupalLogin($translate_user);
$search = array(
'string' => $name,
'langcode' => 'en',
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), 'The translation has been removed');
}
/*
* Adds a language and checks that the JavaScript translation files are
* properly created and rebuilt on deletion.
*/
public function testJavaScriptTranslation() {
$user = $this->drupalCreateUser(array('translate interface', 'administer languages', 'access administration pages'));
$this->drupalLogin($user);
$config = $this->config('locale.settings');
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// Add custom language.
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
$this->container->get('language_manager')->reset();
// Build the JavaScript translation file.
// Retrieve the source string of the first string available in the
// {locales_source} table and translate it.
$source = db_select('locales_source', 'l')
->fields('l', array('source'))
->condition('l.source', '%.js%', 'LIKE')
->range(0, 1)
->execute()
->fetchField();
$search = array(
'string' => $source,
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $this->randomMachineName(),
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Trigger JavaScript translation parsing and building.
_locale_rebuild_js($langcode);
$locale_javascripts = \Drupal::state()->get('locale.translation.javascript') ?: array();
$js_file = 'public://' . $config->get('javascript.directory') . '/' . $langcode . '_' . $locale_javascripts[$langcode] . '.js';
$this->assertTrue($result = file_exists($js_file), SafeMarkup::format('JavaScript file created: %file', array('%file' => $result ? $js_file : 'not found')));
// Test JavaScript translation rebuilding.
file_unmanaged_delete($js_file);
$this->assertTrue($result = !file_exists($js_file), SafeMarkup::format('JavaScript file deleted: %file', array('%file' => $result ? $js_file : 'found')));
_locale_rebuild_js($langcode);
$this->assertTrue($result = file_exists($js_file), SafeMarkup::format('JavaScript file rebuilt: %file', array('%file' => $result ? $js_file : 'not found')));
}
/**
* Tests the validation of the translation input.
*/
public function testStringValidation() {
// User to add language and strings.
$admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// These will be the invalid translations of $name.
$key = $this->randomMachineName(16);
$bad_translations[$key] = "<script>alert('xss');</script>" . $key;
$key = $this->randomMachineName(16);
$bad_translations[$key] = '<img SRC="javascript:alert(\'xss\');">' . $key;
$key = $this->randomMachineName(16);
$bad_translations[$key] = '<<SCRIPT>alert("xss");//<</SCRIPT>' . $key;
$key = $this->randomMachineName(16);
$bad_translations[$key] = "<BODY ONLOAD=alert('xss')>" . $key;
// Add custom language.
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Add string.
t($name, array(), array('langcode' => $langcode));
// Reset locale cache.
$search = array(
'string' => $name,
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
// Find the edit path.
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
foreach ($bad_translations as $translation) {
$edit = array(
$lid => $translation,
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Check for a form error on the textarea.
$form_class = $this->xpath('//form[@id="locale-translate-edit-form"]//textarea/@class');
$this->assertNotIdentical(FALSE, strpos($form_class[0], 'error'), 'The string was rejected as unsafe.');
$this->assertNoText(t('The string has been saved.'), 'The string was not saved.');
}
}
/**
* Tests translation search form.
*/
public function testStringSearch() {
// User to add and remove language.
$admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
// User to translate and delete string.
$translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
// Code for the language.
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// This will be the translation of $name.
$translation = $this->randomMachineName(16);
// Add custom language.
$this->drupalLogin($admin_user);
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => 'yy',
'label' => $this->randomMachineName(16),
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Add string.
t($name, array(), array('langcode' => $langcode));
// Reset locale cache.
$this->container->get('string_translation')->reset();
$this->drupalLogout();
// Search for the name.
$this->drupalLogin($translate_user);
$search = array(
'string' => $name,
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
// assertText() seems to remove the input field where $name always could be
// found, so this is not a false assert. See how assertNoText succeeds
// later.
$this->assertText($name, 'Search found the string.');
// Ensure untranslated string doesn't appear if searching on 'only
// translated strings'.
$search = array(
'string' => $name,
'langcode' => $langcode,
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText(t('No strings available.'), "Search didn't find the string.");
// Ensure untranslated string appears if searching on 'only untranslated
// strings'.
$search = array(
'string' => $name,
'langcode' => $langcode,
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), 'Search found the string.');
// Add translation.
// Assume this is the only result, given the random name.
// We save the lid from the path.
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $translation,
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Ensure translated string does appear if searching on 'only
// translated strings'.
$search = array(
'string' => $translation,
'langcode' => $langcode,
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), 'Search found the translation.');
// Ensure translated source string doesn't appear if searching on 'only
// untranslated strings'.
$search = array(
'string' => $name,
'langcode' => $langcode,
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText(t('No strings available.'), "Search didn't find the source string.");
// Ensure translated string doesn't appear if searching on 'only
// untranslated strings'.
$search = array(
'string' => $translation,
'langcode' => $langcode,
'translation' => 'untranslated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText(t('No strings available.'), "Search didn't find the translation.");
// Ensure translated string does appear if searching on the custom language.
$search = array(
'string' => $translation,
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), 'Search found the translation.');
// Ensure translated string doesn't appear if searching in System (English).
$search = array(
'string' => $translation,
'langcode' => 'yy',
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText(t('No strings available.'), "Search didn't find the translation.");
// Search for a string that isn't in the system.
$unavailable_string = $this->randomMachineName(16);
$search = array(
'string' => $unavailable_string,
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText(t('No strings available.'), "Search didn't find the invalid string.");
}
/**
* Tests that only changed strings are saved customized when edited.
*/
public function testUICustomizedStrings() {
$user = $this->drupalCreateUser(array('translate interface', 'administer languages', 'access administration pages'));
$this->drupalLogin($user);
ConfigurableLanguage::createFromLangcode('de')->save();
// Create test source string.
$string = $this->container->get('locale.storage')->createString(array(
'source' => $this->randomMachineName(100),
'context' => $this->randomMachineName(20),
))->save();
// Create translation for new string and save it as non-customized.
$translation = $this->container->get('locale.storage')->createTranslation(array(
'lid' => $string->lid,
'language' => 'de',
'translation' => $this->randomMachineName(100),
'customized' => 0,
))->save();
// Reset locale cache.
$this->container->get('string_translation')->reset();
// Ensure non-customized translation string does appear if searching
// non-customized translation.
$search = array(
'string' => $string->getString(),
'langcode' => 'de',
'translation' => 'translated',
'customized' => '0',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText($translation->getString(), 'Translation is found in search result.');
// Submit the translations without changing the translation.
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $translation->getString(),
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Ensure unchanged translation string does appear if searching
// non-customized translation.
$search = array(
'string' => $string->getString(),
'langcode' => 'de',
'translation' => 'translated',
'customized' => '0',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText($string->getString(), 'Translation is not marked as customized.');
// Submit the translations with a new translation.
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $this->randomMachineName(100),
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Ensure changed translation string does appear if searching customized
// translation.
$search = array(
'string' => $string->getString(),
'langcode' => 'de',
'translation' => 'translated',
'customized' => '1',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText($string->getString(), "Translation is marked as customized.");
}
}

View file

@ -0,0 +1,313 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleUpdateBase.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
use Drupal\Component\Utility\SafeMarkup;
/**
* Base class for testing updates to string translations.
*/
abstract class LocaleUpdateBase extends WebTestBase {
/**
* Timestamp for an old translation.
*
* @var integer
*/
protected $timestampOld;
/**
* Timestamp for a medium aged translation.
*
* @var integer
*/
protected $timestampMedium;
/**
* Timestamp for a new translation.
*
* @var integer
*/
protected $timestampNew;
/**
* Timestamp for current time.
*
* @var integer
*/
protected $timestampNow;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('update', 'update_test', 'locale', 'locale_test');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Update module should not go out to d.o to check for updates. We override
// the url to the default update_test xml path. But without providing
// a mock xml file, no update data will be found.
$this->config('update.settings')->set('fetch.url', Url::fromRoute('update_test.update_test', [], ['absolute' => TRUE])->toString())->save();
// Setup timestamps to identify old and new translation sources.
$this->timestampOld = REQUEST_TIME - 300;
$this->timestampMedium = REQUEST_TIME - 200;
$this->timestampNew = REQUEST_TIME - 100;
$this->timestampNow = REQUEST_TIME;
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->save();
}
/**
* Sets the value of the default translations directory.
*
* @param string $path
* Path of the translations directory relative to the drupal installation
* directory.
*/
protected function setTranslationsDirectory($path) {
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
$this->config('locale.settings')->set('translation.path', $path)->save();
}
/**
* Adds a language.
*
* @param string $langcode
* The language code of the language to add.
*/
protected function addLanguage($langcode) {
$edit = array('predefined_langcode' => $langcode);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$this->container->get('language_manager')->reset();
$this->assertTrue(\Drupal::languageManager()->getLanguage($langcode), SafeMarkup::format('Language %langcode added.', array('%langcode' => $langcode)));
}
/**
* Creates a translation file and tests its timestamp.
*
* @param string $path
* Path of the file relative to the public file path.
* @param string $filename
* Name of the file to create.
* @param int $timestamp
* (optional) Timestamp to set the file to. Defaults to current time.
* @param array $translations
* (optional) Array of source/target value translation strings. Only
* singular strings are supported, no plurals. No double quotes are allowed
* in source and translations strings.
*/
protected function makePoFile($path, $filename, $timestamp = NULL, array $translations = array()) {
$timestamp = $timestamp ? $timestamp : REQUEST_TIME;
$path = 'public://' . $path;
$text = '';
$po_header = <<<EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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"
EOF;
// Convert array of translations to Gettext source and translation strings.
if ($translations) {
foreach ($translations as $source => $target) {
$text .= 'msgid "' . $source . '"' . "\n";
$text .= 'msgstr "' . $target . '"' . "\n";
}
}
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
$file = entity_create('file', array(
'uid' => 1,
'filename' => $filename,
'uri' => $path . '/' . $filename,
'filemime' => 'text/x-gettext-translation',
'timestamp' => $timestamp,
'status' => FILE_STATUS_PERMANENT,
));
file_put_contents($file->getFileUri(), $po_header . $text);
touch(drupal_realpath($file->getFileUri()), $timestamp);
$file->save();
}
/**
* Setup the environment containing local and remote translation files.
*
* Update tests require a simulated environment for local and remote files.
* Normally remote files are located at a remote server (e.g. ftp.drupal.org).
* For testing we can not rely on this. A directory in the file system of the
* test site is designated for remote files and is addressed using an absolute
* URL. Because Drupal does not allow files with a po extension to be accessed
* (denied in .htaccess) the translation files get a _po extension. Another
* directory is designated for local translation files.
*
* The environment is set up with the following files. File creation times are
* set to create different variations in test conditions.
* contrib_module_one
* - remote file: timestamp new
* - local file: timestamp old
* contrib_module_two
* - remote file: timestamp old
* - local file: timestamp new
* contrib_module_three
* - remote file: timestamp old
* - local file: timestamp old
* custom_module_one
* - local file: timestamp new
* Time stamp of current translation set by setCurrentTranslations() is always
* timestamp medium. This makes it easy to predict which translation will be
* imported.
*/
protected function setTranslationFiles() {
$config = $this->config('locale.settings');
// A flag is set to let the locale_test module replace the project data with
// a set of test projects which match the below project files.
\Drupal::state()->set('locale.test_projects_alter', TRUE);
// Setup the environment.
$public_path = PublicStream::basePath();
$this->setTranslationsDirectory($public_path . '/local');
$config->set('translation.default_filename', '%project-%version.%language._po')->save();
// Setting up sets of translations for the translation files.
$translations_one = array('January' => 'Januar_1', 'February' => 'Februar_1', 'March' => 'Marz_1');
$translations_two = array('February' => 'Februar_2', 'March' => 'Marz_2', 'April' => 'April_2');
$translations_three = array('April' => 'April_3', 'May' => 'Mai_3', 'June' => 'Juni_3');
// Add a number of files to the local file system to serve as remote
// translation server and match the project definitions set in
// locale_test_locale_translation_projects_alter().
$this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de._po', $this->timestampNew, $translations_one);
$this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestampOld, $translations_two);
$this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de._po', $this->timestampOld, $translations_three);
// Add a number of files to the local file system to serve as local
// translation files and match the project definitions set in
// locale_test_locale_translation_projects_alter().
$this->makePoFile('local', 'contrib_module_one-8.x-1.1.de._po', $this->timestampOld, $translations_one);
$this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestampNew, $translations_two);
$this->makePoFile('local', 'contrib_module_three-8.x-1.0.de._po', $this->timestampOld, $translations_three);
$this->makePoFile('local', 'custom_module_one.de.po', $this->timestampNew);
}
/**
* Setup existing translations in the database and set up the status of
* existing translations.
*/
protected function setCurrentTranslations() {
// Add non customized translations to the database.
$langcode = 'de';
$context = '';
$non_customized_translations = array(
'March' => 'Marz',
'June' => 'Juni',
);
foreach ($non_customized_translations as $source => $translation) {
$string = $this->container->get('locale.storage')->createString(array(
'source' => $source,
'context' => $context,
))
->save();
$this->container->get('locale.storage')->createTranslation(array(
'lid' => $string->getId(),
'language' => $langcode,
'translation' => $translation,
'customized' => LOCALE_NOT_CUSTOMIZED,
))->save();
}
// Add customized translations to the database.
$customized_translations = array(
'January' => 'Januar_customized',
'February' => 'Februar_customized',
'May' => 'Mai_customized',
);
foreach ($customized_translations as $source => $translation) {
$string = $this->container->get('locale.storage')->createString(array(
'source' => $source,
'context' => $context,
))
->save();
$this->container->get('locale.storage')->createTranslation(array(
'lid' => $string->getId(),
'language' => $langcode,
'translation' => $translation,
'customized' => LOCALE_CUSTOMIZED,
))->save();
}
// Add a state of current translations in locale_files.
$default = array(
'langcode' => $langcode,
'uri' => '',
'timestamp' => $this->timestampMedium,
'last_checked' => $this->timestampMedium,
);
$data[] = array(
'project' => 'contrib_module_one',
'filename' => 'contrib_module_one-8.x-1.1.de._po',
'version' => '8.x-1.1',
);
$data[] = array(
'project' => 'contrib_module_two',
'filename' => 'contrib_module_two-8.x-2.0-beta4.de._po',
'version' => '8.x-2.0-beta4',
);
$data[] = array(
'project' => 'contrib_module_three',
'filename' => 'contrib_module_three-8.x-1.0.de._po',
'version' => '8.x-1.0',
);
$data[] = array(
'project' => 'custom_module_one',
'filename' => 'custom_module_one.de.po',
'version' => '',
);
foreach ($data as $file) {
$file = array_merge($default, $file);
db_insert('locale_file')->fields($file)->execute();
}
}
/**
* Checks the translation of a string.
*
* @param string $source
* Translation source string.
* @param string $translation
* Translation to check. Use empty string to check for a not existing
* translation.
* @param string $langcode
* Language code of the language to translate to.
* @param string $message
* (optional) A message to display with the assertion.
*/
protected function assertTranslation($source, $translation, $langcode, $message = '') {
$db_translation = db_query('SELECT translation FROM {locales_target} lt INNER JOIN {locales_source} ls ON ls.lid = lt.lid WHERE ls.source = :source AND lt.language = :langcode', array(':source' => $source, ':langcode' => $langcode))->fetchField();
$db_translation = $db_translation == FALSE ? '' : $db_translation;
$this->assertEqual($translation, $db_translation, $message ? $message : format_string('Correct translation of %source (%language)', array('%source' => $source, '%language' => $langcode)));
}
}

View file

@ -0,0 +1,114 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleUpdateCronTest.
*/
namespace Drupal\locale\Tests;
/**
* Tests for using cron to update project interface translations.
*
* @group locale
*/
class LocaleUpdateCronTest extends LocaleUpdateBase {
protected $batchOutput = array();
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
$this->addLanguage('de');
}
/**
* Tests interface translation update using cron.
*/
public function testUpdateCron() {
// Set a flag to let the locale_test module replace the project data with a
// set of test projects.
\Drupal::state()->set('locale.test_projects_alter', TRUE);
// Setup local and remote translations files.
$this->setTranslationFiles();
$this->config('locale.settings')->set('translation.default_filename', '%project-%version.%language._po')->save();
// Update translations using batch to ensure a clean test starting point.
$this->drupalGet('admin/reports/translations/check');
$this->drupalPostForm('admin/reports/translations', array(), t('Update translations'));
// Store translation status for comparison.
$initial_history = locale_translation_get_file_history();
// Prepare for test: Simulate new translations being available.
// Change the last updated timestamp of a translation file.
$contrib_module_two_uri = 'public://local/contrib_module_two-8.x-2.0-beta4.de._po';
touch(drupal_realpath($contrib_module_two_uri), REQUEST_TIME);
// Prepare for test: Simulate that the file has not been checked for a long
// time. Set the last_check timestamp to zero.
$query = db_update('locale_file');
$query->fields(array('last_checked' => 0));
$query->condition('project', 'contrib_module_two');
$query->condition('langcode', 'de');
$query->execute();
// Test: Disable cron update and verify that no tasks are added to the
// queue.
$edit = array(
'update_interval_days' => 0,
);
$this->drupalPostForm('admin/config/regional/translate/settings', $edit, t('Save configuration'));
// Execute locale cron tasks to add tasks to the queue.
locale_cron();
// Check whether no tasks are added to the queue.
$queue = \Drupal::queue('locale_translation', TRUE);
$this->assertEqual($queue->numberOfItems(), 0, 'Queue is empty');
// Test: Enable cron update and check if update tasks are added to the
// queue.
// Set cron update to Weekly.
$edit = array(
'update_interval_days' => 7,
);
$this->drupalPostForm('admin/config/regional/translate/settings', $edit, t('Save configuration'));
// Execute locale cron tasks to add tasks to the queue.
locale_cron();
// Check whether tasks are added to the queue.
$queue = \Drupal::queue('locale_translation', TRUE);
$this->assertEqual($queue->numberOfItems(), 3, 'Queue holds tasks for one project.');
$item = $queue->claimItem();
$queue->releaseItem($item);
$this->assertEqual($item->data[1][0], 'contrib_module_two', 'Queue holds tasks for contrib module one.');
// Test: Run cron for a second time and check if tasks are not added to
// the queue twice.
locale_cron();
// Check whether no more tasks are added to the queue.
$queue = \Drupal::queue('locale_translation', TRUE);
$this->assertEqual($queue->numberOfItems(), 3, 'Queue holds tasks for one project.');
// Ensure last checked is updated to a greater time than the initial value.
sleep(1);
// Test: Execute cron and check if tasks are executed correctly.
// Run cron to process the tasks in the queue.
$this->cronRun();
drupal_static_reset('locale_translation_get_file_history');
$history = locale_translation_get_file_history();
$initial = $initial_history['contrib_module_two']['de'];
$current = $history['contrib_module_two']['de'];
$this->assertTrue($current->timestamp > $initial->timestamp, 'Timestamp is updated');
$this->assertTrue($current->last_checked > $initial->last_checked, 'Last checked is updated');
}
}

View file

@ -0,0 +1,120 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleUpdateInterfaceTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Component\Utility\SafeMarkup;
/**
* Tests for the user interface of project interface translations.
*
* @group locale
*/
class LocaleUpdateInterfaceTest extends LocaleUpdateBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale_test_translate');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
}
/**
* Tests the user interfaces of the interface translation update system.
*
* Testing the Available updates summary on the side wide status page and the
* Available translation updates page.
*/
public function testInterface() {
// No language added.
// Check status page and Available translation updates page.
$this->drupalGet('admin/reports/status');
$this->assertNoText(t('Translation update status'), 'No status message');
$this->drupalGet('admin/reports/translations');
$this->assertRaw(t('No translatable languages available. <a href="@add_language">Add a language</a> first.', array('@add_language' => \Drupal::url('entity.configurable_language.collection'))), 'Language message');
// Add German language.
$this->addLanguage('de');
// Override Drupal core translation status as 'up-to-date'.
$status = locale_translation_get_status();
$status['drupal']['de']->type = 'current';
\Drupal::state()->set('locale.translation_status', $status);
// One language added, all translations up to date.
$this->drupalGet('admin/reports/status');
$this->assertText(t('Translation update status'), 'Status message');
$this->assertText(t('Up to date'), 'Translations up to date');
$this->drupalGet('admin/reports/translations');
$this->assertText(t('All translations up to date.'), 'Translations up to date');
// Set locale_test_translate module to have a local translation available.
$status = locale_translation_get_status();
$status['locale_test_translate']['de']->type = 'local';
\Drupal::state()->set('locale.translation_status', $status);
// Check if updates are available for German.
$this->drupalGet('admin/reports/status');
$this->assertText(t('Translation update status'), 'Status message');
$this->assertRaw(t('Updates available for: @languages. See the <a href="@updates">Available translation updates</a> page for more information.', array('@languages' => t('German'), '@updates' => \Drupal::url('locale.translate_status'))), 'Updates available message');
$this->drupalGet('admin/reports/translations');
$this->assertText(t('Updates for: @modules', array('@modules' => 'Locale test translate')), 'Translations available');
// Set locale_test_translate module to have a dev release and no
// translation found.
$status = locale_translation_get_status();
$status['locale_test_translate']['de']->version = '1.3-dev';
$status['locale_test_translate']['de']->type = '';
\Drupal::state()->set('locale.translation_status', $status);
// Check if no updates were found.
$this->drupalGet('admin/reports/status');
$this->assertText(t('Translation update status'), 'Status message');
$this->assertRaw(t('Missing translations for: @languages. See the <a href="@updates">Available translation updates</a> page for more information.', array('@languages' => t('German'), '@updates' => \Drupal::url('locale.translate_status'))), 'Missing translations message');
$this->drupalGet('admin/reports/translations');
$this->assertText(t('Missing translations for one project'), 'No translations found');
$this->assertText(SafeMarkup::format('@module (@version). !info', array('@module' => 'Locale test translate', '@version' => '1.3-dev', '!info' => t('No translation files are provided for development releases.'))), 'Release details');
$this->assertText(t('No translation files are provided for development releases.'), 'Release info');
// Override Drupal core translation status as 'no translations found'.
$status = locale_translation_get_status();
$status['drupal']['de']->type = '';
$status['drupal']['de']->timestamp = 0;
$status['drupal']['de']->version = '8.1.1';
\Drupal::state()->set('locale.translation_status', $status);
// Check if Drupal core is not translated.
$this->drupalGet('admin/reports/translations');
$this->assertText(t('Missing translations for 2 projects'), 'No translations found');
$this->assertText(t('@module (@version).', array('@module' => t('Drupal core'), '@version' => '8.1.1')), 'Release details');
// Override Drupal core translation status as 'translations available'.
$status = locale_translation_get_status();
$status['drupal']['de']->type = 'local';
$status['drupal']['de']->files['local']->timestamp = REQUEST_TIME;
$status['drupal']['de']->files['local']->info['version'] = '8.1.1';
\Drupal::state()->set('locale.translation_status', $status);
// Check if translations are available for Drupal core.
$this->drupalGet('admin/reports/translations');
$this->assertText(t('Updates for: !project', array('!project' => t('Drupal core'))), 'Translations found');
$this->assertText(SafeMarkup::format('@module (@date)', array('@module' => t('Drupal core'), '@date' => format_date(REQUEST_TIME, 'html_date'))), 'Core translation update');
$update_button = $this->xpath('//input[@type="submit"][@value="' . t('Update translations') . '"]');
$this->assertTrue($update_button, 'Update translations button');
}
}

View file

@ -0,0 +1,83 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleUpdateNotDevelopmentReleaseTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Test for finding the first available normal core release version,
* in case of core is a development release.
*
* @group language
*/
class LocaleUpdateNotDevelopmentReleaseTest extends WebTestBase {
public static $modules = array('update', 'locale', 'locale_test_not_development_release');
protected function setUp() {
parent::setUp();
module_load_include('compare.inc', 'locale');
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
$this->drupalPostForm('admin/config/regional/language/add', array('predefined_langcode' => 'hu'), t('Add language'));
}
public function testLocaleUpdateNotDevelopmentRelease() {
// Set available Drupal releases for test.
$available = array(
'title' => 'Drupal core',
'short_name' => 'drupal',
'type' => 'project_core',
'api_version' => '8.x',
'project_status' => 'unsupported',
'link' => 'https://www.drupal.org/project/drupal',
'terms' => '',
'releases' => array(
'8.0.0-alpha110' => array(
'name' => 'drupal 8.0.0-alpha110',
'version' => '8.0.0-alpha110',
'tag' => '8.0.0-alpha110',
'version_major' => '8',
'version_minor' => '0',
'version_patch' => '0',
'version_extra' => 'alpha110',
'status' => 'published',
'release_link' => 'https://www.drupal.org/node/2316617',
'download_link' => 'http://ftp.drupal.org/files/projects/drupal-8.0.0-alpha110.tar.gz',
'date' => '1407344628',
'mdhash' => '9d71afdd0ce541f2ff5ca2fbbca00df7',
'filesize' => '9172832',
'files' => '',
'terms' => array(),
),
'8.0.0-alpha100' => array(
'name' => 'drupal 8.0.0-alpha100',
'version' => '8.0.0-alpha100',
'tag' => '8.0.0-alpha100',
'version_major' => '8',
'version_minor' => '0',
'version_patch' => '0',
'version_extra' => 'alpha100',
'status' => 'published',
'release_link' => 'https://www.drupal.org/node/2316617',
'download_link' => 'http://ftp.drupal.org/files/projects/drupal-8.0.0-alpha100.tar.gz',
'date' => '1407344628',
'mdhash' => '9d71afdd0ce541f2ff5ca2fbbca00df7',
'filesize' => '9172832',
'files' => '',
'terms' => array(),
),
),
);
$available['last_fetch'] = REQUEST_TIME;
\Drupal::keyValueExpirable('update_available_releases')->setWithExpire('drupal', $available, 10);
$projects = locale_translation_build_projects();
$this->verbose($projects['drupal']->info['version']);
$this->assertEqual($projects['drupal']->info['version'], '8.0.0-alpha110', 'The first release with the same major release number which is not a development release.');
}
}

View file

@ -0,0 +1,447 @@
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleUpdateTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\Language\LanguageInterface;
/**
* Tests for updating the interface translations of projects.
*
* @group locale
*/
class LocaleUpdateTest extends LocaleUpdateBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
module_load_include('compare.inc', 'locale');
module_load_include('fetch.inc', 'locale');
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
// We use German as test language. This language must match the translation
// file that come with the locale_test module (test.de.po) and can therefore
// not be chosen randomly.
$this->addLanguage('de');
}
/**
* Checks if a list of translatable projects gets build.
*/
public function testUpdateProjects() {
module_load_include('compare.inc', 'locale');
// Make the test modules look like a normal custom module. i.e. make the
// modules not hidden. locale_test_system_info_alter() modifies the project
// info of the locale_test and locale_test_translate modules.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
$this->resetAll();
// Check if interface translation data is collected from hook_info.
$projects = locale_translation_project_list();
$this->assertFalse(isset($projects['locale_test_translate']), 'Hidden module not found');
$this->assertEqual($projects['locale_test']['info']['interface translation server pattern'], 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.');
$this->assertEqual($projects['locale_test']['name'], 'locale_test', format_string('%key found in project info.', array('%key' => 'interface translation project')));
}
/**
* Checks if local or remote translation sources are detected.
*
* The translation status process by default checks the status of the
* installed projects. For testing purpose a predefined set of modules with
* fixed file names and release versions is used. This custom project
* definition is applied using a hook_locale_translation_projects_alter
* implementation in the locale_test module.
*
* This test generates a set of local and remote translation files in their
* respective local and remote translation directory. The test checks whether
* the most recent files are selected in the different check scenarios: check
* for local files only, check for both local and remote files.
*/
public function testUpdateCheckStatus() {
// Case when contributed modules are absent.
$this->drupalGet('admin/reports/translations');
$this->assertText(t('Missing translations for one project'));
$config = $this->config('locale.settings');
// Set a flag to let the locale_test module replace the project data with a
// set of test projects.
\Drupal::state()->set('locale.test_projects_alter', TRUE);
// Create local and remote translations files.
$this->setTranslationFiles();
$config->set('translation.default_filename', '%project-%version.%language._po')->save();
// Set the test conditions.
$edit = array(
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_LOCAL,
);
$this->drupalPostForm('admin/config/regional/translate/settings', $edit, t('Save configuration'));
// Get status of translation sources at local file system.
$this->drupalGet('admin/reports/translations/check');
$result = locale_translation_get_status();
$this->assertEqual($result['contrib_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_one found');
$this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestampOld, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_two']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_two found');
$this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestampNew, 'Translation timestamp found');
$this->assertEqual($result['locale_test']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of locale_test found');
$this->assertEqual($result['custom_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of custom_module_one found');
// Set the test conditions.
$edit = array(
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
);
$this->drupalPostForm('admin/config/regional/translate/settings', $edit, t('Save configuration'));
// Get status of translation sources at both local and remote locations.
$this->drupalGet('admin/reports/translations/check');
$result = locale_translation_get_status();
$this->assertEqual($result['contrib_module_one']['de']->type, LOCALE_TRANSLATION_REMOTE, 'Translation of contrib_module_one found');
$this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestampNew, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_two']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_two found');
$this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestampNew, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_three']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_three found');
$this->assertEqual($result['contrib_module_three']['de']->timestamp, $this->timestampOld, 'Translation timestamp found');
$this->assertEqual($result['locale_test']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of locale_test found');
$this->assertEqual($result['custom_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of custom_module_one found');
}
/**
* Tests translation import from remote sources.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: all existing translations
*/
public function testUpdateImportSourceRemote() {
$config = $this->config('locale.settings');
// Build the test environment.
$this->setTranslationFiles();
$this->setCurrentTranslations();
$config->set('translation.default_filename', '%project-%version.%language._po');
// Set the update conditions for this test.
$edit = array(
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL,
);
$this->drupalPostForm('admin/config/regional/translate/settings', $edit, t('Save configuration'));
// Get the translation status.
$this->drupalGet('admin/reports/translations/check');
// Check the status on the Available translation status page.
$this->assertRaw('<label for="edit-langcodes-de" class="visually-hidden">Update German</label>', 'German language found');
$this->assertText('Updates for: Contributed module one, Contributed module two, Custom module one, Locale test', 'Updates found');
$this->assertText('Contributed module one (' . format_date($this->timestampNow, 'html_date') . ')', 'Updates for Contrib module one');
$this->assertText('Contributed module two (' . format_date($this->timestampNew, 'html_date') . ')', 'Updates for Contrib module two');
// Execute the translation update.
$this->drupalPostForm('admin/reports/translations', array(), t('Update translations'));
// Check if the translation has been updated, using the status cache.
$status = locale_translation_get_status();
$this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found');
$this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found');
$this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found');
// Check the new translation status.
// The static cache needs to be flushed first to get the most recent data
// from the database. The function was called earlier during this test.
drupal_static_reset('locale_translation_get_file_history');
$history = locale_translation_get_file_history();
$this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestampNow, 'Translation of contrib_module_one is imported');
$this->assertTrue($history['contrib_module_one']['de']->last_checked >= $this->timestampNow, 'Translation of contrib_module_one is updated');
$this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestampNew, 'Translation of contrib_module_two is imported');
$this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestampNow, 'Translation of contrib_module_two is updated');
$this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestampMedium, 'Translation of contrib_module_three is not imported');
$this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestampMedium, 'Translation of contrib_module_three is not updated');
// Check whether existing translations have (not) been overwritten.
$this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_1', 'Translation of January');
$this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February');
$this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
$this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
$this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
$this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
$this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
}
/**
* Tests translation import from local sources.
*
* Test conditions:
* - Source: local files only
* - Import overwrite: all existing translations
*/
public function testUpdateImportSourceLocal() {
$config = $this->config('locale.settings');
// Build the test environment.
$this->setTranslationFiles();
$this->setCurrentTranslations();
$config->set('translation.default_filename', '%project-%version.%language._po');
// Set the update conditions for this test.
$edit = array(
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_LOCAL,
'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL,
);
$this->drupalPostForm('admin/config/regional/translate/settings', $edit, t('Save configuration'));
// Execute the translation update.
$this->drupalGet('admin/reports/translations/check');
$this->drupalPostForm('admin/reports/translations', array(), t('Update translations'));
// Check if the translation has been updated, using the status cache.
$status = locale_translation_get_status();
$this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found');
$this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found');
$this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found');
// Check the new translation status.
// The static cache needs to be flushed first to get the most recent data
// from the database. The function was called earlier during this test.
drupal_static_reset('locale_translation_get_file_history');
$history = locale_translation_get_file_history();
$this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestampMedium, 'Translation of contrib_module_one is imported');
$this->assertEqual($history['contrib_module_one']['de']->last_checked, $this->timestampMedium, 'Translation of contrib_module_one is updated');
$this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestampNew, 'Translation of contrib_module_two is imported');
$this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestampNow, 'Translation of contrib_module_two is updated');
$this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestampMedium, 'Translation of contrib_module_three is not imported');
$this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestampMedium, 'Translation of contrib_module_three is not updated');
// Check whether existing translations have (not) been overwritten.
$this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January');
$this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February');
$this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
$this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
$this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
$this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
$this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
}
/**
* Tests translation import and only overwrite non-customized translations.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: only overwrite non-customized translations
*/
public function testUpdateImportModeNonCustomized() {
$config = $this->config('locale.settings');
// Build the test environment.
$this->setTranslationFiles();
$this->setCurrentTranslations();
$config->set('translation.default_filename', '%project-%version.%language._po');
// Set the test conditions.
$edit = array(
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED,
);
$this->drupalPostForm('admin/config/regional/translate/settings', $edit, t('Save configuration'));
// Execute translation update.
$this->drupalGet('admin/reports/translations/check');
$this->drupalPostForm('admin/reports/translations', array(), t('Update translations'));
// Check whether existing translations have (not) been overwritten.
$this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January');
$this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_customized', 'Translation of February');
$this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
$this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
$this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
$this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
$this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
}
/**
* Tests translation import and don't overwrite any translation.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: don't overwrite any existing translation
*/
public function testUpdateImportModeNone() {
$config = $this->config('locale.settings');
// Build the test environment.
$this->setTranslationFiles();
$this->setCurrentTranslations();
$config->set('translation.default_filename', '%project-%version.%language._po');
// Set the test conditions.
$edit = array(
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => LOCALE_TRANSLATION_OVERWRITE_NONE,
);
$this->drupalPostForm('admin/config/regional/translate/settings', $edit, t('Save configuration'));
// Execute translation update.
$this->drupalGet('admin/reports/translations/check');
$this->drupalPostForm('admin/reports/translations', array(), t('Update translations'));
// Check whether existing translations have (not) been overwritten.
$this->assertTranslation('January', 'Januar_customized', 'de');
$this->assertTranslation('February', 'Februar_customized', 'de');
$this->assertTranslation('March', 'Marz', 'de');
$this->assertTranslation('April', 'April_2', 'de');
$this->assertTranslation('May', 'Mai_customized', 'de');
$this->assertTranslation('June', 'Juni', 'de');
$this->assertTranslation('Monday', 'Montag', 'de');
}
/**
* Tests automatic translation import when a module is enabled.
*/
public function testEnableUninstallModule() {
// Make the hidden test modules look like a normal custom module.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
// Check if there is no translation yet.
$this->assertTranslation('Tuesday', '', 'de');
// Enable a module.
$edit = array(
'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate',
);
$this->drupalPostForm('admin/modules', $edit, t('Save configuration'));
// Check if translations have been imported.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
array('%number' => 7, '%update' => 0, '%delete' => 0)), 'One translation file imported.');
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
$edit = array(
'uninstall[locale_test_translate]' => 1,
);
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->drupalPostForm(NULL, array(), t('Uninstall'));
// Check if the file data is removed from the database.
$history = locale_translation_get_file_history();
$this->assertFalse(isset($history['locale_test_translate']), 'Project removed from the file history');
$projects = locale_translation_get_projects();
$this->assertFalse(isset($projects['locale_test_translate']), 'Project removed from the project list');
}
/**
* Tests automatic translation import when a language is added.
*
* When a language is added, the system will check for translations files of
* enabled modules and will import them. When a language is removed the system
* will remove all translations of that language from the database.
*/
public function testEnableLanguage() {
// Make the hidden test modules look like a normal custom module.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
// Enable a module.
$edit = array(
'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate',
);
$this->drupalPostForm('admin/modules', $edit, t('Save configuration'));
// Check if there is no Dutch translation yet.
$this->assertTranslation('Extraday', '', 'nl');
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
// Add a language.
$edit = array(
'predefined_langcode' => 'nl',
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// Check if the right number of translations are added.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
array('%number' => 8, '%update' => 0, '%delete' => 0)), 'One language added.');
$this->assertTranslation('Extraday', 'extra dag', 'nl');
// Check if the language data is added to the database.
$result = db_query("SELECT project FROM {locale_file} WHERE langcode='nl'")->fetchField();
$this->assertTrue($result, 'Files added to file history');
// Remove a language.
$this->drupalPostForm('admin/config/regional/language/delete/nl', array(), t('Delete'));
// Check if the language data is removed from the database.
$result = db_query("SELECT project FROM {locale_file} WHERE langcode='nl'")->fetchField();
$this->assertFalse($result, 'Files removed from file history');
// Check that the Dutch translation is gone.
$this->assertTranslation('Extraday', '', 'nl');
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
}
/**
* Tests automatic translation import when a custom language is added.
*/
public function testEnableCustomLanguage() {
// Make the hidden test modules look like a normal custom module.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
// Enable a module.
$edit = array(
'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate',
);
$this->drupalPostForm('admin/modules', $edit, t('Save configuration'));
// Create a custom language with language code 'xx' and a random
// name.
$langcode = 'xx';
$name = $this->randomMachineName(16);
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
// Ensure the translation file is automatically imported when the language
// was added.
$this->assertText(t('One translation file imported.'), 'Language file automatically imported.');
$this->assertText(t('One translation string was skipped because of disallowed or malformed HTML'), 'Language file automatically imported.');
// Ensure the strings were successfully imported.
$search = array(
'string' => 'lundi',
'langcode' => $langcode,
'translation' => 'translated',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), 'String successfully imported.');
// Ensure the multiline string was imported.
$search = array(
'string' => 'Source string for multiline translation',
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText('Multiline translation string to make sure that import works with it.', 'String successfully imported.');
// Ensure 'Allowed HTML source string' was imported but the translation for
// 'Another allowed HTML source string' was not because it contains invalid
// HTML.
$search = array(
'string' => 'HTML source string',
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertText('Allowed HTML source string', 'String successfully imported.');
$this->assertNoText('Another allowed HTML source string', 'String with disallowed translation not imported.');
}
}

View file

@ -0,0 +1,128 @@
<?php
/**
* @file
* Contains \Drupal\locale\TranslationString.
*/
namespace Drupal\locale;
/**
* Defines the locale translation string object.
*
* This class represents a translation of a source string to a given language,
* thus it must have at least a 'language' which is the language code and a
* 'translation' property which is the translated text of the source string
* in the specified language.
*/
class TranslationString extends StringBase {
/**
* The language code.
*
* @var string
*/
public $language;
/**
* The string translation.
*
* @var string
*/
public $translation;
/**
* Integer indicating whether this string is customized.
*
* @var int
*/
public $customized;
/**
* Boolean indicating whether the string object is new.
*
* @var bool
*/
protected $isNew;
/**
* Overrides Drupal\locale\StringBase::__construct().
*/
public function __construct($values = array()) {
parent::__construct($values);
if (!isset($this->isNew)) {
// We mark the string as not new if it is a complete translation.
// This will work when loading from database, otherwise the storage
// controller that creates the string object must handle it.
$this->isNew = !$this->isTranslation();
}
}
/**
* Sets the string as customized / not customized.
*
* @param bool $customized
* (optional) Whether the string is customized or not. Defaults to TRUE.
*
* @return \Drupal\locale\TranslationString
* The called object.
*/
public function setCustomized($customized = TRUE) {
$this->customized = $customized ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED;
return $this;
}
/**
* Implements Drupal\locale\StringInterface::isSource().
*/
public function isSource() {
return FALSE;
}
/**
* Implements Drupal\locale\StringInterface::isTranslation().
*/
public function isTranslation() {
return !empty($this->lid) && !empty($this->language) && isset($this->translation);
}
/**
* Implements Drupal\locale\StringInterface::getString().
*/
public function getString() {
return isset($this->translation) ? $this->translation : '';
}
/**
* Implements Drupal\locale\StringInterface::setString().
*/
public function setString($string) {
$this->translation = $string;
return $this;
}
/**
* Implements Drupal\locale\StringInterface::isNew().
*/
public function isNew() {
return $this->isNew;
}
/**
* Implements Drupal\locale\StringInterface::save().
*/
public function save() {
parent::save();
$this->isNew = FALSE;
return $this;
}
/**
* Implements Drupal\locale\StringInterface::delete().
*/
public function delete() {
parent::delete();
$this->isNew = TRUE;
return $this;
}
}

View file

@ -0,0 +1,25 @@
{#
/**
* @file
* Default theme implementation for the last time we checked for update data.
*
* Available variables:
* - last_checked: Whether or not locale updates have been checked before.
* - time: The formatted time ago when the site last checked for available
* updates.
* - link: A link to manually check available updates.
*
* @see template_preprocess_locale_translation_last_check()
*
* @ingroup themeable
*/
#}
<div class="locale checked">
<p>
{% if last_checked %}
{% trans %} Last checked: {{ time }} ago {% endtrans %}
{% else %}
{{ 'Last checked: never'|t }}
{% endif %}
<span class="check-manually">({{ link }})</span></p>
</div>

View file

@ -0,0 +1,32 @@
{#
/**
* @file
* Default theme implementation for displaying translation status information.
*
* Displays translation status information per language.
*
* Available variables:
* - modules: A list of names of modules that have available translation
* updates.
* - details: Rendered list of the translation details.
* - missing_updates_status: If there are any modules that are missing
* translation updates, this variable will contain text indicating how many
* modules are missing translations.
*
* @see template_preprocess_locale_translation_update_info()
*
* @ingroup themeable
*/
#}
<div class="locale-translation-update__wrapper" tabindex="0" role="button">
<span class="locale-translation-update__prefix visually-hidden">Show description</span>
{% if modules %}
{% set module_list = modules|safe_join(', ') %}
<span class="locale-translation-update__message">{% trans %}Updates for: {{ module_list }}{% endtrans %}</span>
{% elseif missing_updates_status %}
<span class="locale-translation-update__message">{{ missing_updates_status }}</span>
{% endif %}
{% if details %}
<div class="locale-translation-update__details">{{ details }}</div>
{% endif %}
</div>

View file

@ -0,0 +1,52 @@
/**
* @file
* JavaScript for locale_test.module.
*
* @ignore
*/
Drupal.t("Standard Call t");
Drupal
.
t
(
"Whitespace Call t"
)
;
Drupal.t('Single Quote t');
Drupal.t('Single Quote \'Escaped\' t');
Drupal.t('Single Quote ' + 'Concat ' + 'strings ' + 't');
Drupal.t("Double Quote t");
Drupal.t("Double Quote \"Escaped\" t");
Drupal.t("Double Quote " + "Concat " + "strings " + "t");
Drupal.t("Context Unquoted t", {}, {context: "Context string unquoted"});
Drupal.t("Context Single Quoted t", {}, {'context': "Context string single quoted"});
Drupal.t("Context Double Quoted t", {}, {"context": "Context string double quoted"});
Drupal.t("Context !key Args t", {'!key': 'value'}, {context: "Context string"});
Drupal.formatPlural(1, "Standard Call plural", "Standard Call @count plural");
Drupal
.
formatPlural
(
1,
"Whitespace Call plural",
"Whitespace Call @count plural"
)
;
Drupal.formatPlural(1, 'Single Quote plural', 'Single Quote @count plural');
Drupal.formatPlural(1, 'Single Quote \'Escaped\' plural', 'Single Quote \'Escaped\' @count plural');
Drupal.formatPlural(1, "Double Quote plural", "Double Quote @count plural");
Drupal.formatPlural(1, "Double Quote \"Escaped\" plural", "Double Quote \"Escaped\" @count plural");
Drupal.formatPlural(1, "Context Unquoted plural", "Context Unquoted @count plural", {}, {context: "Context string unquoted"});
Drupal.formatPlural(1, "Context Single Quoted plural", "Context Single Quoted @count plural", {}, {'context': "Context string single quoted"});
Drupal.formatPlural(1, "Context Double Quoted plural", "Context Double Quoted @count plural", {}, {"context": "Context string double quoted"});
Drupal.formatPlural(1, "Context !key Args plural", "Context !key Args @count plural", {'!key': 'value'}, {context: "Context string"});

View file

@ -0,0 +1,7 @@
name: 'Early translation test'
type: module
description: 'Support module for testing early bootstrap getting of annotations with translations.'
core: 8.x
package: Testing
version: VERSION

View file

@ -0,0 +1,6 @@
services:
early_translation_test.authentication.early_translation_test:
class: Drupal\early_translation_test\Auth
arguments: ['@entity.manager']
tags:
- { name: authentication_provider, provider_id: 'early_translation_test', priority: 100 }

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\early_translation_test\Auth.
*/
namespace Drupal\early_translation_test;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Test authentication provider.
*/
class Auth implements AuthenticationProviderInterface {
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs an authentication provider object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
*/
public function __construct(EntityManagerInterface $entity_manager) {
// Authentication providers are called early during in the bootstrap.
// Getting the user storage used to result in a circular reference since
// translation involves a call to \Drupal\locale\LocaleLookup that tries to
// get the user roles.
// @see https://www.drupal.org/node/2241461
$this->userStorage = $entity_manager->getStorage('user');
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
return NULL;
}
}

View file

@ -0,0 +1 @@
test: English test

View file

@ -0,0 +1,21 @@
# Schema for the configuration files of the Locale Test module.
locale_test.no_translation:
type: config_object
label: 'No traslation settings'
mapping:
test:
type: string
label: 'Test'
# See \Drupal\locale\Tests\LocaleConfigSubscriberTest
translatable: true
locale_test.translation:
type: config_object
label: 'translation settings'
mapping:
test:
type: string
label: 'Test'
# See \Drupal\locale\Tests\LocaleConfigSubscriberTest
translatable: true

View file

@ -0,0 +1,9 @@
name: 'Locale test'
type: module
description: 'Support module for locale module testing.'
package: Testing
version: '1.2'
core: 8.x
hidden: true
'interface translation project': locale_test
'interface translation server pattern': core/modules/locale/test/test.%language.po

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Install, update and uninstall functions for the locale_test module.
*/
/**
* Implements hook_uninstall().
*/
function locale_test_uninstall() {
// Clear variables.
\Drupal::state()->delete('locale.test_system_info_alter');
\Drupal::state()->delete('locale.test_projects_alter');
}

View file

@ -0,0 +1,144 @@
<?php
/**
* @file
* Simulate a custom module with a local po file.
*/
use Drupal\Core\Extension\Extension;
use Drupal\Core\StreamWrapper\PublicStream;
/**
* Implements hook_system_info_alter().
*
* Make the test scripts to be believe this is not a hidden test module, but
* a regular custom module.
*/
function locale_test_system_info_alter(&$info, Extension $file, $type) {
// Only modify the system info if required.
// By default the locale_test modules are hidden and have a project specified.
// To test the module detection process by locale_project_list() the
// test modules should mimic a custom module. I.e. be non-hidden.
if (\Drupal::state()->get('locale.test_system_info_alter')) {
if ($file->getName() == 'locale_test' || $file->getName() == 'locale_test_translate') {
// Don't hide the module.
$info['hidden'] = FALSE;
}
}
}
/**
* Implements hook_locale_translation_projects_alter().
*
* The translation status process by default checks the status of the installed
* projects. This function replaces the data of the installed modules by a
* predefined set of modules with fixed file names and release versions. Project
* names, versions, timestamps etc must be fixed because they must match the
* files created by the test script.
*
* The "locale.test_projects_alter" state variable must be set by the
* test script in order for this hook to take effect.
*/
function locale_test_locale_translation_projects_alter(&$projects) {
// Drupal core should not be translated. By overriding the server pattern we
// make sure that no translation for drupal core will be found and that the
// translation update system will not go out to l.d.o to check.
$projects['drupal']['server_pattern'] = 'translations://';
if (\Drupal::state()->get('locale.test_projects_alter')) {
// Instead of the default ftp.drupal.org we use the file system of the test
// instance to simulate a remote file location.
$url = \Drupal::url('<front>', [], ['absolute' => TRUE]);
$remote_url = $url . PublicStream::basePath() . '/remote/';
// Completely replace the project data with a set of test projects.
$projects = array(
'contrib_module_one' => array(
'name' => 'contrib_module_one',
'info' => array(
'name' => 'Contributed module one',
'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
'package' => 'Other',
'version' => '8.x-1.1',
'project' => 'contrib_module_one',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
),
'datestamp' => '1344471537',
'project_type' => 'module',
'project_status' => TRUE,
),
'contrib_module_two' => array(
'name' => 'contrib_module_two',
'info' => array(
'name' => 'Contributed module two',
'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
'package' => 'Other',
'version' => '8.x-2.0-beta4',
'project' => 'contrib_module_two',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
),
'datestamp' => '1344471537',
'project_type' => 'module',
'project_status' => TRUE,
),
'contrib_module_three' => array(
'name' => 'contrib_module_three',
'info' => array(
'name' => 'Contributed module three',
'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
'package' => 'Other',
'version' => '8.x-1.0',
'project' => 'contrib_module_three',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
),
'datestamp' => '1344471537',
'project_type' => 'module',
'project_status' => TRUE,
),
'locale_test' => array(
'name' => 'locale_test',
'info' => array(
'name' => 'Locale test',
'interface translation project' => 'locale_test',
'interface translation server pattern' => 'core/modules/locale/tests/test.%language.po',
'package' => 'Other',
'version' => NULL,
'project' => 'locale_test',
'_info_file_ctime' => 1348767306,
'datestamp' => 0,
),
'datestamp' => 0,
'project_type' => 'module',
'project_status' => TRUE,
),
'custom_module_one' => array(
'name' => 'custom_module_one',
'info' => array(
'name' => 'Custom module one',
'interface translation project' => 'custom_module_one',
'interface translation server pattern' => 'translations://custom_module_one.%language.po',
'package' => 'Other',
'version' => NULL,
'project' => 'custom_module_one',
'_info_file_ctime' => 1348767306,
'datestamp' => 0,
),
'datestamp' => 0,
'project_type' => 'module',
'project_status' => TRUE,
),
);
}
}
/**
* Implements hook_language_fallback_candidates_OPERATION_alter().
*/
function locale_test_language_fallback_candidates_locale_lookup_alter(array &$candidates, array $context) {
\Drupal::state()->set('locale.test_language_fallback_candidates_locale_lookup_alter_candidates', $candidates);
\Drupal::state()->set('locale.test_language_fallback_candidates_locale_lookup_alter_context', $context);
}

View file

@ -0,0 +1,7 @@
name: 'Locale Test Not Development Release'
type: module
description: 'The first release with the same major release number which is not a development release.'
package: Testing
version: VERSION
core: 8.x
hidden: true

View file

@ -0,0 +1,20 @@
<?php
/**
* @file
* Simulate a Drupal version.
*/
use Drupal\Core\Extension\Extension;
/**
* Implements hook_system_info_alter().
*
* Change the core version number to a development one for testing.
* 8.0.0-alpha102-dev is the simulated version.
*/
function locale_test_not_development_release_system_info_alter(&$info, Extension $file, $type) {
if (isset($info['package']) && $info['package'] == 'Core') {
$info['version'] = '8.0.0-alpha102-dev';
}
}

View file

@ -0,0 +1,9 @@
name: 'Locale test translate'
type: module
description: 'Translation test module for locale module testing.'
package: Testing
version: '1.3'
core: 8.x
hidden: true
'interface translation project': locale_test_translate
'interface translation server pattern': core/modules/locale/tests/test.%language.po

View file

@ -0,0 +1,22 @@
<?php
/**
* @file
* Simulates a custom module with a local po file.
*/
use Drupal\Core\Extension\Extension;
/**
* Implements hook_system_info_alter().
*
* By default this modules is hidden but once enabled it behaves like a normal
* (not hidden) module. This hook implementation changes the .info.yml data by
* setting the hidden status to FALSE.
*/
function locale_test_translate_system_info_alter(&$info, Extension $file, $type) {
if ($file->getName() == 'locale_test_translate') {
// Don't hide the module.
$info['hidden'] = FALSE;
}
}

View file

@ -0,0 +1,274 @@
<?php
/**
* @file
* Contains \Drupal\Tests\locale\Unit\LocaleLookupTest.
*/
namespace Drupal\Tests\locale\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\locale\LocaleLookup;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @coversDefaultClass \Drupal\locale\LocaleLookup
* @group locale
*/
class LocaleLookupTest extends UnitTestCase {
/**
* A mocked storage to use when instantiating LocaleTranslation objects.
*
* @var \Drupal\locale\StringStorageInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $storage;
/**
* A mocked cache object.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cache;
/**
* A mocked lock object.
*
* @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $lock;
/**
* A mocked user object built from AccountInterface.
*
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $user;
/**
* A mocked config factory built with UnitTestCase::getConfigFactoryStub().
*
* @var \Drupal\Core\Config\ConfigFactory|\PHPUnit_Framework_MockObject_MockBuilder
*/
protected $configFactory;
/**
* A mocked language manager built from LanguageManagerInterface.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $languageManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->storage = $this->getMock('Drupal\locale\StringStorageInterface');
$this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
$this->lock->expects($this->never())
->method($this->anything());
$this->user = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->user->expects($this->any())
->method('getRoles')
->will($this->returnValue(array('anonymous')));
$this->configFactory = $this->getConfigFactoryStub(array('locale.settings' => array('cache_strings' => FALSE)));
$this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
$this->requestStack = new RequestStack();
$container = new ContainerBuilder();
$container->set('current_user', $this->user);
\Drupal::setContainer($container);
}
/**
* Tests locale lookups without fallback.
*
* @covers ::resolveCacheMiss
*/
public function testResolveCacheMissWithoutFallback() {
$args = array(
'language' => 'en',
'source' => 'test',
'context' => 'irrelevant',
);
$result = (object) array(
'translation' => 'test',
);
$this->cache->expects($this->once())
->method('get')
->with('locale:en:irrelevant:0', FALSE);
$this->storage->expects($this->once())
->method('findTranslation')
->with($this->equalTo($args))
->will($this->returnValue($result));
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(array('en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack))
->setMethods(array('persist'))
->getMock();
$locale_lookup->expects($this->never())
->method('persist');
$this->assertSame('test', $locale_lookup->get('test'));
}
/**
* Tests locale lookups with fallback.
*
* Note that context is irrelevant here. It is not used but it is required.
*
* @covers ::resolveCacheMiss
*
* @dataProvider resolveCacheMissWithFallbackProvider
*/
public function testResolveCacheMissWithFallback($langcode, $string, $context, $expected) {
// These are fake words!
$translations = array(
'en' => array(
'test' => 'test',
'fake' => 'fake',
'missing pl' => 'missing pl',
'missing cs' => 'missing cs',
'missing both' => 'missing both',
),
'pl' => array(
'test' => 'test po polsku',
'fake' => 'ściema',
'missing cs' => 'zaginiony czech',
),
'cs' => array(
'test' => 'test v české',
'fake' => 'falešný',
'missing pl' => 'chybějící pl',
),
);
$this->storage->expects($this->any())
->method('findTranslation')
->will($this->returnCallback(function ($argument) use ($translations) {
if (isset($translations[$argument['language']][$argument['source']])) {
return (object) array('translation' => $translations[$argument['language']][$argument['source']]);
}
return TRUE;
}));
$this->languageManager->expects($this->any())
->method('getFallbackCandidates')
->will($this->returnCallback(function (array $context = array()) {
switch ($context['langcode']) {
case 'pl':
return array('cs', 'en');
case 'cs':
return array('en');
default:
return array();
}
}));
$this->cache->expects($this->once())
->method('get')
->with('locale:' . $langcode . ':' . $context . ':0', FALSE);
$locale_lookup = new LocaleLookup($langcode, $context, $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack);
$this->assertSame($expected, $locale_lookup->get($string));
}
/**
* Provides test data for testResolveCacheMissWithFallback().
*/
public function resolveCacheMissWithFallbackProvider() {
return array(
array('cs', 'test', 'irrelevant', 'test v české'),
array('cs', 'fake', 'irrelevant', 'falešný'),
array('cs', 'missing pl', 'irrelevant', 'chybějící pl'),
array('cs', 'missing cs', 'irrelevant', 'missing cs'),
array('cs', 'missing both', 'irrelevant', 'missing both'),
// Testing PL with fallback to cs, en.
array('pl', 'test', 'irrelevant', 'test po polsku'),
array('pl', 'fake', 'irrelevant', 'ściema'),
array('pl', 'missing pl', 'irrelevant', 'chybějící pl'),
array('pl', 'missing cs', 'irrelevant', 'zaginiony czech'),
array('pl', 'missing both', 'irrelevant', 'missing both'),
);
}
/**
* Tests locale lookups with persistent tracking.
*
* @covers ::resolveCacheMiss
*/
public function testResolveCacheMissWithPersist() {
$args = array(
'language' => 'en',
'source' => 'test',
'context' => 'irrelevant',
);
$result = (object) array(
'translation' => 'test',
);
$this->storage->expects($this->once())
->method('findTranslation')
->with($this->equalTo($args))
->will($this->returnValue($result));
$this->configFactory = $this->getConfigFactoryStub(array('locale.settings' => array('cache_strings' => TRUE)));
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(array('en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack))
->setMethods(array('persist'))
->getMock();
$locale_lookup->expects($this->once())
->method('persist');
$this->assertSame('test', $locale_lookup->get('test'));
}
/**
* Tests locale lookups without a found translation.
*
* @covers ::resolveCacheMiss
*/
public function testResolveCacheMissNoTranslation() {
$string = $this->getMock('Drupal\locale\StringInterface');
$string->expects($this->once())
->method('addLocation')
->will($this->returnSelf());
$this->storage->expects($this->once())
->method('findTranslation')
->will($this->returnValue(NULL));
$this->storage->expects($this->once())
->method('createString')
->will($this->returnValue($string));
$request = Request::create('/test');
$this->requestStack->push($request);
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(array('en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack))
->setMethods(array('persist'))
->getMock();
$locale_lookup->expects($this->never())
->method('persist');
$this->assertTrue($locale_lookup->get('test'));
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\Tests\locale\Unit\LocaleTranslationTest.
*/
namespace Drupal\Tests\locale\Unit;
use Drupal\locale\LocaleTranslation;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @coversDefaultClass \Drupal\locale\LocaleTranslation
* @group locale
*/
class LocaleTranslationTest extends UnitTestCase {
/**
* A mocked storage to use when instantiating LocaleTranslation objects.
*
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $storage;
/**
* A mocked language manager built from LanguageManagerInterface.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $languageManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->storage = $this->getMock('Drupal\locale\StringStorageInterface');
$this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
$this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
$this->requestStack = new RequestStack();
}
/**
* Tests for \Drupal\locale\LocaleTranslation::destruct().
*/
public function testDestruct() {
$translation = new LocaleTranslation($this->storage, $this->cache, $this->lock, $this->getConfigFactoryStub(), $this->languageManager, $this->requestStack);
// Prove that destruction works without errors when translations are empty.
$this->assertAttributeEmpty('translations', $translation);
$translation->destruct();
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\Tests\locale\Unit\Menu\LocaleLocalTasksTest.
*/
namespace Drupal\Tests\locale\Unit\Menu;
use Drupal\Tests\Core\Menu\LocalTaskIntegrationTestBase;
/**
* Tests locale local tasks.
*
* @group locale
*/
class LocaleLocalTasksTest extends LocalTaskIntegrationTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->directoryList = array(
'locale' => 'core/modules/locale',
);
parent::setUp();
}
/**
* Checks locale listing local tasks.
*
* @dataProvider getLocalePageRoutes
*/
public function testLocalePageLocalTasks($route) {
$tasks = array(
0 => array('locale.translate_page', 'locale.translate_import', 'locale.translate_export','locale.settings'),
);
$this->assertLocalTasks($route, $tasks);
}
/**
* Provides a list of routes to test.
*/
public function getLocalePageRoutes() {
return array(
array('locale.translate_page'),
array('locale.translate_import'),
array('locale.translate_export'),
array('locale.settings'),
);
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\Tests\locale\Unit\StringBaseTest.
*/
namespace Drupal\Tests\locale\Unit;
use Drupal\locale\SourceString;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\locale\StringBase
* @group locale
*/
class StringBaseTest extends UnitTestCase {
/**
* @covers ::save
* @expectedException \Drupal\locale\StringStorageException
* @expectedExceptionMessage The string cannot be saved because its not bound to a storage: test
*/
public function testSaveWithoutStorage() {
$string = new SourceString(['source' => 'test']);
$string->save();
}
/**
* @covers ::delete
* @expectedException \Drupal\locale\StringStorageException
* @expectedExceptionMessage The string cannot be deleted because its not bound to a storage: test
*/
public function testDeleteWithoutStorage() {
$string = new SourceString(['lid' => 1, 'source' => 'test']);
$string->delete();
}
}

View file

@ -0,0 +1,10 @@
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\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 "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
msgstr "Ons is tans besig met onderhoud op @site. Wees asseblief geduldig, ons sal binnekort weer terug wees."

Some files were not shown because too many files have changed in this diff Show more