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,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;
}
}