Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\locale\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* 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 [
'filter' => $this->formBuilder()->getForm('Drupal\locale\Form\TranslateFilterForm'),
'form' => $this->formBuilder()->getForm('Drupal\locale\Form\TranslateEditForm'),
];
}
}

View file

@ -0,0 +1,46 @@
<?php
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,187 @@
<?php
namespace Drupal\locale\Form;
use Drupal\Component\Gettext\PoStreamWriter;
use Drupal\Core\File\FileSystemInterface;
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.
*
* @internal
*/
class ExportForm extends FormBase {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* Constructs a new ExportForm.
*
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
*/
public function __construct(LanguageManagerInterface $language_manager, FileSystemInterface $file_system) {
$this->languageManager = $language_manager;
$this->fileSystem = $file_system;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('language_manager'),
$container->get('file_system')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'locale_translate_export_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$languages = $this->languageManager->getLanguages();
$language_options = [];
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'] = [
'#type' => 'value',
'#value' => LanguageInterface::LANGCODE_SYSTEM,
];
$form['langcode_text'] = [
'#type' => 'item',
'#title' => $this->t('Language'),
'#markup' => $this->t('No language available. The export will only contain source strings.'),
];
}
else {
$form['langcode'] = [
'#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'] = [
'#type' => 'details',
'#title' => $this->t('Export options'),
'#tree' => TRUE,
'#states' => [
'invisible' => [
':input[name="langcode"]' => ['value' => LanguageInterface::LANGCODE_SYSTEM],
],
],
];
$form['content_options']['not_customized'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include non-customized translations'),
'#default_value' => TRUE,
];
$form['content_options']['customized'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include customized translations'),
'#default_value' => TRUE,
];
$form['content_options']['not_translated'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include untranslated text'),
'#default_value' => TRUE,
];
}
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#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', []);
$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 = $this->fileSystem->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 {
$this->messenger()->addStatus($this->t('Nothing to export.'));
}
}
}

View file

@ -0,0 +1,199 @@
<?php
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.
*
* @internal
*/
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 = [];
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 = [
(string) $this->t('Existing languages') => $existing_languages,
(string) $this->t('Languages not yet added') => $this->languageManager->getStandardLanguageListWithoutConfigured(),
];
}
$validators = [
'file_validate_extensions' => ['po'],
'file_validate_size' => [file_upload_max_size()],
];
$form['file'] = [
'#type' => 'file',
'#title' => $this->t('Translation file'),
'#description' => [
'#theme' => 'file_upload_help',
'#description' => $this->t('A Gettext Portable Object file.'),
'#upload_validators' => $validators,
],
'#size' => 50,
'#upload_validators' => $validators,
'#upload_location' => 'translations://',
'#attributes' => ['class' => ['file-import-input']],
];
$form['langcode'] = [
'#type' => 'select',
'#title' => $this->t('Language'),
'#options' => $language_options,
'#default_value' => $default,
'#attributes' => ['class' => ['langcode-input']],
];
$form['customized'] = [
'#title' => $this->t('Treat imported strings as custom translations'),
'#type' => 'checkbox',
];
$form['overwrite_options'] = [
'#type' => 'container',
'#tree' => TRUE,
];
$form['overwrite_options']['not_customized'] = [
'#title' => $this->t('Overwrite non-customized translations'),
'#type' => 'checkbox',
'#states' => [
'checked' => [
':input[name="customized"]' => ['checked' => TRUE],
],
],
];
$form['overwrite_options']['customized'] = [
'#title' => $this->t('Overwrite existing customized translations'),
'#type' => 'checkbox',
];
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Import'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$this->file = _file_save_upload_from_form($form['file'], $form_state, 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) {
$this->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();
$this->messenger()->addStatus($this->t('The language %language has been created.', ['%language' => $this->t($language->label())]));
}
$options = array_merge(_locale_translation_default_update_options(), [
'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([$file->uri => $file], $options);
batch_set($batch);
// Create or update all configuration translations for this language.
if ($batch = locale_config_batch_update_components($options, [$form_state->getValue('langcode')])) {
batch_set($batch);
}
$form_state->setRedirect('locale.translate_page');
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace Drupal\locale\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure locale settings for this site.
*
* @internal
*/
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'] = [
'#type' => 'radios',
'#title' => $this->t('Check for updates'),
'#default_value' => $config->get('translation.update_interval_days'),
'#options' => [
'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>.', [':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.', ['%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.', [':url' => $this->url('system.file_system_settings')]);
}
$form['#translation_directory'] = $directory;
$form['use_source'] = [
'#type' => 'radios',
'#title' => $this->t('Translation source'),
'#default_value' => $config->get('translation.use_source'),
'#options' => [
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'] = [
'#type' => 'radios',
'#title' => $this->t('Import behavior'),
'#default_value' => $default,
'#options' => [
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);
}
/**
* {@inheritdoc}
*/
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.', [':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,241 @@
<?php
namespace Drupal\locale\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\locale\SourceString;
/**
* Defines a translation edit form.
*
* @internal
*/
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'] = [
'#type' => 'value',
'#value' => $filter_values['langcode'],
];
$form['strings'] = [
'#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'] = [
'#type' => 'item',
'#title' => $this->t('Source string (@language)', ['@language' => $this->t('Built-in English')]),
'#title_display' => 'invisible',
'#plain_text' => $source_array[0],
'#preffix' => '<span lang="en">',
'#suffix' => '</span>',
];
}
else {
// Add original string value and mark as plural.
$plural = TRUE;
$original_singular = [
'#type' => 'item',
'#title' => $this->t('Singular form'),
'#plain_text' => $source_array[0],
'#prefix' => '<span class="visually-hidden">' . $this->t('Source string (@language)', ['@language' => $this->t('Built-in English')]) . '</span><span lang="en">',
'#suffix' => '</span>',
];
$original_plural = [
'#type' => 'item',
'#title' => $this->t('Plural form'),
'#plain_text' => $source_array[1],
'#preffix' => '<span lang="en">',
'#suffix' => '</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] = [
'#type' => 'textarea',
'#title' => $this->t('Translated string (@language)', ['@language' => $langname]),
'#title_display' => 'invisible',
'#rows' => $rows,
'#default_value' => $translation_array[0],
'#attributes' => ['lang' => $langcode],
];
}
else {
// Add a textarea for each plural variant.
for ($i = 0; $i < $plurals; $i++) {
$form['strings'][$string->lid]['translations'][$i] = [
'#type' => 'textarea',
// @todo Should use better labels https://www.drupal.org/node/2499639
'#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' => ['lang' => $langcode],
'#prefix' => $i == 0 ? ('<span class="visually-hidden">' . $this->t('Translated string (@language)', ['@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'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#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', ['%string' => $value]));
$form_state->setErrorByName("translations][$langcode][$key", $this->t('The submitted string contains disallowed HTML: %string', ['%string' => $value]));
$this->logger('locale')->warning('Attempted submission of a translation string with disallowed HTML: %string', ['%string' => $value]);
}
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$langcode = $form_state->getValue('langcode');
$updated = [];
// Preload all translations for strings in the form.
$lids = array_keys($form_state->getValue('strings'));
$existing_translation_objects = [];
foreach ($this->localeStorage->getTranslations(['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(['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;
}
}
$this->messenger()->addStatus($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',
[],
['page' => $page]
);
}
if ($updated) {
// Clear cache and force refresh of JavaScript translations.
_locale_refresh_translations([$langcode], $updated);
_locale_refresh_configuration([$langcode], $updated);
}
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Drupal\locale\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a filtered translation edit form.
*
* @internal
*/
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'] = [
'#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'] = [
'#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] = [
'#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'] = [
'#type' => 'actions',
'#attributes' => ['class' => ['container-inline']],
];
$form['filters']['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Filter'),
];
if (!empty($_SESSION['locale_translate_filter'])) {
$form['filters']['actions']['reset'] = [
'#type' => 'submit',
'#value' => $this->t('Reset'),
'#submit' => ['::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'] = [];
$form_state->setRedirect('locale.translate_page');
}
}

View file

@ -0,0 +1,214 @@
<?php
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 = ['language' => $filter_values['langcode']];
$options = ['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 = [];
$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 = [];
// Get all languages, except English.
$this->languageManager->reset();
$languages = $this->languageManager->getLanguages();
$language_options = [];
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'] = [
'title' => $this->t('String contains'),
'description' => $this->t('Leave blank to show all strings. The search is case sensitive.'),
'default' => '',
];
$filters['langcode'] = [
'title' => $this->t('Translation language'),
'options' => $language_options,
'default' => $default_langcode,
];
$filters['translation'] = [
'title' => $this->t('Search in'),
'options' => [
'all' => $this->t('Both translated and untranslated strings'),
'translated' => $this->t('Only translated strings'),
'untranslated' => $this->t('Only untranslated strings'),
],
'default' => 'all',
];
$filters['customized'] = [
'title' => $this->t('Translation type'),
'options' => [
'all' => $this->t('All'),
LOCALE_NOT_CUSTOMIZED => $this->t('Non-customized translation'),
LOCALE_CUSTOMIZED => $this->t('Customized translation'),
],
'states' => [
'visible' => [
':input[name=translation]' => ['value' => 'translated'],
],
],
'default' => 'all',
];
return $filters;
}
}

View file

@ -0,0 +1,298 @@
<?php
namespace Drupal\locale\Form;
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.
*
* @internal
*/
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 \Drupal\Core\Extension\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 = [];
$languages_update = [];
$languages_not_found = [];
$projects_update = [];
// 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 = $languages[$langcode]->getName();
$locale_translation_update_info = ['#theme' => 'locale_translation_update_info'];
foreach (['updates', 'not_found'] as $update_status) {
if (isset($update[$update_status])) {
$locale_translation_update_info['#' . $update_status] = $update[$update_status];
}
}
$options[$langcode] = [
'title' => [
'data' => [
'#title' => $title,
'#plain_text' => $title,
],
],
'status' => [
'class' => ['description', 'priority-low'],
'data' => $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'] = [
'#theme' => 'locale_translation_last_check',
'#last' => $last_checked,
];
$header = [
'title' => [
'data' => $this->t('Language'),
'class' => ['title'],
],
'status' => [
'data' => $this->t('Status'),
'class' => ['status', 'priority-low'],
],
];
if (!$languages) {
$empty = $this->t('No translatable languages available. <a href=":add_language">Add a language</a> first.', [
':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>.', [
':check' => $this->url('locale.check_translation'),
]);
}
// The projects which require an update. Used by the _submit callback.
$form['projects_update'] = [
'#type' => 'value',
'#value' => $projects_update,
];
$form['langcodes'] = [
'#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' => ['locale_translation_language_table'],
];
$form['#attached']['library'][] = 'locale/drupal.locale.admin';
$form['actions'] = ['#type' => 'actions'];
if ($languages_update) {
$form['actions']['submit'] = [
'#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 = [];
// @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'][] = [
'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'][] = [
'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.
*
* @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 (locale_translation_use_remote_source() && $remote_path && $local_path) {
return $this->t('File not found at %remote_path nor at %local_path', [
'%remote_path' => $remote_path,
'%local_path' => $local_path,
]);
}
elseif ($local_path) {
return $this->t('File not found at %local_path', ['%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([], $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,97 @@
<?php
namespace Drupal\locale;
use Drupal\Component\Gettext\PoStreamReader;
/**
* 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 += [
'overwrite_options' => [],
'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 = [
'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,24 @@
<?php
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,651 @@
<?php
namespace Drupal\locale;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
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;
/**
* The configuration manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* 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.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration manager.
*/
public function __construct(StorageInterface $config_storage, StringStorageInterface $locale_storage, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config, ConfigurableLanguageManagerInterface $language_manager, LocaleDefaultConfigStorage $default_config_storage, ConfigManagerInterface $config_manager) {
$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;
$this->configManager = $config_manager;
}
/**
* Gets array of translated strings for Locale translatable configuration.
*
* @param string $name
* Configuration object name.
*
* @return array
* Array of Locale 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);
$typed_config = $this->typedConfigManager->createFromNameAndData($name, $data);
if ($typed_config instanceof TraversableTypedDataInterface) {
return $this->getTranslatableData($typed_config);
}
}
return [];
}
/**
* Gets translatable configuration data for a typed configuration element.
*
* @param \Drupal\Core\TypedData\TypedDataInterface $element
* Typed configuration element.
*
* @return array|\Drupal\Core\StringTranslation\TranslatableMarkup
* A nested array matching the exact structure under $element with only the
* elements that are translatable wrapped into a TranslatableMarkup. If the
* provided $element is not traversable, the return value is a single
* TranslatableMarkup.
*/
protected function getTranslatableData(TypedDataInterface $element) {
$translatable = [];
if ($element instanceof TraversableTypedDataInterface) {
foreach ($element as $key => $property) {
$value = $this->getTranslatableData($property);
if (!empty($value)) {
$translatable[$key] = $value;
}
}
}
else {
// Something is only translatable by Locale if there is a string in the
// first place.
$value = $element->getValue();
$definition = $element->getDataDefinition();
if (!empty($definition['translatable']) && $value !== '' && $value !== NULL) {
$options = [];
if (isset($definition['translation context'])) {
$options['context'] = $definition['translation context'];
}
return new TranslatableMarkup($value, [], $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\TranslatableMarkup[] $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 TranslatableMarkup.
* @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.
*
* @see self::getTranslatableData()
*/
protected function processTranslatableData($name, array $active, array $translatable, $langcode) {
$translated = [];
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 = []) {
$components = array_filter($components);
if ($components) {
$names = [];
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 = [];
$locations = $this->localeStorage->getLocations(['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] = [];
foreach ($this->localeStorage->getTranslations(['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(['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(['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 = [];
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 = ['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) {
// Config entities that do not have the 'default_config_hash' cannot be
// shipped configuration regardless of whether there is a name match.
// configurable_language entities are a special case since they can be
// translated regardless of whether they are shipped if they in the standard
// language list.
$config_entity_type = $this->configManager->getEntityTypeIdByName($name);
if (!$config_entity_type || $config_entity_type === 'configurable_language'
|| !empty($this->configFactory->get($name)->get('_core.default_config_hash'))
) {
$shipped = $this->defaultConfigStorage->read($name);
if (!empty($shipped)) {
return !empty($shipped['langcode']) ? $shipped['langcode'] : 'en';
}
}
return NULL;
}
/**
* 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 = []) {
$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 the language code is not the same as the active storage
// language, we should update the configuration override.
if ($langcode != $active_langcode) {
$override = $this->languageManager->getLanguageConfigOverride($langcode, $name);
// Filter out locale managed configuration keys so that translations
// removed from Locale will be reflected in the config override.
$data = $this->filterOverride($override->get(), $translatable);
if (!empty($processed)) {
// Merge in the Locale managed translations with existing data.
$data = NestedArray::mergeDeepArray([$data, $processed], TRUE);
}
if (empty($data) && !$override->isNew()) {
// The configuration override contains Locale overrides that no
// longer exist.
$this->deleteTranslationOverride($name, $langcode);
$count++;
}
elseif (!empty($data)) {
// Update translation data in configuration override.
$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([$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 = [];
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,228 @@
<?php
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 = []) {
$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\TranslatableMarkup[] $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 = []) {
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] : [];
$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\TranslatableMarkup $translatable
* Either a possibly nested array with TranslatableMarkup objects at the
* leaf items or a TranslatableMarkup 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,159 @@
<?php
namespace Drupal\locale;
use Drupal\Core\Config\ExtensionInstallStorage;
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, $install_profile) {
$this->configStorage = $config_storage;
$this->languageManager = $language_manager;
$this->requiredInstallStorage = new ExtensionInstallStorage($this->configStorage, ExtensionInstallStorage::CONFIG_INSTALL_DIRECTORY, ExtensionInstallStorage::DEFAULT_COLLECTION, TRUE, $install_profile);
$this->optionalInstallStorage = new ExtensionInstallStorage($this->configStorage, ExtensionInstallStorage::CONFIG_OPTIONAL_DIRECTORY, ExtensionInstallStorage::DEFAULT_COLLECTION, TRUE, $install_profile);
}
/**
* 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,57 @@
<?php
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 = []) {
$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,24 @@
<?php
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,193 @@
<?php
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 Drupal\Core\StringTranslation\PluralTranslatableMarkup;
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 = ['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(':', $user->getRoles()) : '';
$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 = [];
}
return $this->cid;
}
/**
* {@inheritdoc}
*/
protected function resolveCacheMiss($offset) {
$translation = $this->stringStorage->findTranslation([
'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([
'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(['langcode' => $this->langcode, 'operation' => 'locale_lookup', 'data' => $offset]);
if (!empty($fallbacks)) {
foreach ($fallbacks as $langcode) {
$translation = $this->stringStorage->findTranslation([
'language' => $langcode,
'source' => $offset,
'context' => $this->context,
]);
if ($translation && !empty($translation->translation)) {
$value = $translation->translation;
break;
}
}
}
}
if (is_string($value) && strpos($value, PluralTranslatableMarkup::DELIMITER) !== FALSE) {
// Community translations imported from localize.drupal.org as well as
// migrated translations may contain @count[number].
$value = preg_replace('!@count\[\d+\]!', '@count', $value);
}
$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,169 @@
<?php
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 = [];
/**
* 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.
*/
public function __construct(KeyValueFactoryInterface $key_value_factory) {
$this->keyValueStore = $key_value_factory->get('locale.project');
}
/**
* {@inheritdoc}
*/
public function get($key, $default = NULL) {
$values = $this->getMultiple([$key]);
return isset($values[$key]) ? $values[$key] : $default;
}
/**
* {@inheritdoc}
*/
public function getMultiple(array $keys) {
$values = [];
$load = [];
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([$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([$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 = [];
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,102 @@
<?php
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,156 @@
<?php
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 = [];
/**
* 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 = [];
}
/**
* {@inheritdoc}
*/
public function destruct() {
foreach ($this->translations as $context) {
foreach ($context as $lookup) {
if ($lookup instanceof DestructableInterface) {
$lookup->destruct();
}
}
}
}
}

View file

@ -0,0 +1,117 @@
<?php
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,111 @@
<?php
namespace Drupal\locale;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\State\StateInterface;
/**
* Manages the storage of plural formula per language in state.
*
* @see \Drupal\locale\PoDatabaseWriter::setHeader()
*/
class PluralFormula implements PluralFormulaInterface {
/**
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The plural formula and count keyed by langcode.
*
* For example the structure looks like this:
* @code
* [
* 'de' => [
* 'plurals' => 2,
* 'formula' => [
* // @todo
* ]
* ],
* ]
* @endcode
* @var array
*/
protected $formulae;
/**
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* @param \Drupal\Core\State\StateInterface $state
*/
public function __construct(LanguageManagerInterface $language_manager, StateInterface $state) {
$this->languageManager = $language_manager;
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function setPluralFormula($langcode, $plural_count, array $formula) {
// Ensure that the formulae are loaded.
$this->loadFormulae();
$this->formulae[$langcode] = [
'plurals' => $plural_count,
'formula' => $formula,
];
$this->state->set('locale.translation.formulae', $this->formulae);
return $this;
}
/**
* {@inheritdoc}
*/
public function getNumberOfPlurals($langcode = NULL) {
// Ensure that the formulae are loaded.
$this->loadFormulae();
// Set the langcode to use.
$langcode = $langcode ?: $this->languageManager->getCurrentLanguage()->getId();
// We assume 2 plurals if there is no explicit information yet.
if (!isset($this->formulae[$langcode]['plurals'])) {
return 2;
}
return $this->formulae[$langcode]['plurals'];
}
/**
* {@inheritdoc}
*/
public function getFormula($langcode) {
$this->loadFormulae();
return isset($this->formulae[$langcode]['formula']) ? $this->formulae[$langcode]['formula'] : FALSE;
}
/**
* Loads the formulae and stores them on the PluralFormula object if not set.
*
* @return array
*/
protected function loadFormulae() {
if (!isset($this->formulae)) {
$this->formulae = $this->state->get('locale.translation.formulae', []);
}
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->formulae = NULL;
return $this;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Drupal\locale;
/**
* An interface for a service providing plural formulae.
*/
interface PluralFormulaInterface {
/**
* @param string $langcode
* The language code to get the formula for.
* @param int $plural_count
* The number of plural forms.
* @param array $formula
* An array of formulae.
*
* @return self
* The PluralFormula object.
*/
public function setPluralFormula($langcode, $plural_count, array $formula);
/**
* Returns the number of plurals supported by a given language.
*
* @param null|string $langcode
* (optional) The language code. If not provided, the current language
* will be used.
*
* @return int
* Number of plural variants supported by the given language.
*/
public function getNumberOfPlurals($langcode = NULL);
/**
* Gets the plural formula for a langcode.
*
* @param string $langcode
* The language code to get the formula for.
*
* @return array
* An array of formulae.
*/
public function getFormula($langcode);
/**
* Resets the static formulae cache.
*
* @return self
* The PluralFormula object.
*/
public function reset();
}

View file

@ -0,0 +1,171 @@
<?php
namespace Drupal\locale;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoReaderInterface;
/**
* 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([]);
}
/**
* {@inheritdoc}
*/
public function getLangcode() {
return $this->langcode;
}
/**
* {@inheritdoc}
*/
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 += [
'customized' => FALSE,
'not_customized' => FALSE,
'not_translated' => FALSE,
];
$this->options = $options;
}
/**
* {@inheritdoc}
*/
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 = [];
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);
}
/**
* {@inheritdoc}
*/
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,289 @@
<?php
namespace Drupal\locale;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoReaderInterface;
use Drupal\Component\Gettext\PoWriterInterface;
/**
* 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:
* - overwrite_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();
}
/**
* {@inheritdoc}
*/
public function getLangcode() {
return $this->langcode;
}
/**
* {@inheritdoc}
*/
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 = []) {
$report += [
'additions' => 0,
'updates' => 0,
'deletes' => 0,
'skips' => 0,
'strings' => [],
];
$this->report = $report;
}
/**
* Get the options used by the writer.
*/
public function getOptions() {
return $this->options;
}
/**
* Set the options for the current writer.
*
* @param array $options
* An associative array containing:
* - overwrite_options: An array of options. Each option contains:
* - 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.
*/
public function setOptions(array $options) {
if (!isset($options['overwrite_options'])) {
$options['overwrite_options'] = [];
}
$options['overwrite_options'] += [
'not_customized' => FALSE,
'customized' => FALSE,
];
$options += [
'customized' => LOCALE_NOT_CUSTOMIZED,
];
$this->options = $options;
}
/**
* {@inheritdoc}
*/
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') ?: [];
// 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;
\Drupal::service('locale.plural.formula')->setPluralFormula($langcode, $nplurals, $formula);
}
}
}
/**
* {@inheritdoc}
*/
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);
}
/**
* {@inheritdoc}
*/
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'] += [
'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([
'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.', ['%string' => $translation]);
$this->report['skips']++;
return 0;
}
elseif ($string) {
$string->setString($translation);
if ($string->isNew()) {
// No translation in this language.
$string->setValues([
'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(['source' => $source, 'context' => $context])
->save();
\Drupal::service('locale.storage')->createTranslation([
'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,50 @@
<?php
namespace Drupal\locale;
/**
* 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 {
/**
* {@inheritdoc}
*/
public function isSource() {
return isset($this->source);
}
/**
* {@inheritdoc}
*/
public function isTranslation() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getString() {
return isset($this->source) ? $this->source : '';
}
/**
* {@inheritdoc}
*/
public function setString($string) {
$this->source = $string;
return $this;
}
/**
* {@inheritdoc}
*/
public function isNew() {
return empty($this->lid);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\locale\StreamWrapper;
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');
}
/**
* {@inheritdoc}
*/
public 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.
*/
public function getExternalUrl() {
throw new \LogicException('PO files URL should not be public.');
}
}

View file

@ -0,0 +1,206 @@
<?php
namespace Drupal\locale;
/**
* 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 int
*/
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 = []) {
$this->setValues((array) $values);
}
/**
* {@inheritdoc}
*/
public function getId() {
return isset($this->lid) ? $this->lid : NULL;
}
/**
* {@inheritdoc}
*/
public function setId($lid) {
$this->lid = $lid;
return $this;
}
/**
* {@inheritdoc}
*/
public function getVersion() {
return isset($this->version) ? $this->version : NULL;
}
/**
* {@inheritdoc}
*/
public function setVersion($version) {
$this->version = $version;
return $this;
}
/**
* {@inheritdoc}
*/
public function getPlurals() {
return explode(LOCALE_PLURAL_DELIMITER, $this->getString());
}
/**
* {@inheritdoc}
*/
public function setPlurals($plurals) {
$this->setString(implode(LOCALE_PLURAL_DELIMITER, $plurals));
return $this;
}
/**
* {@inheritdoc}
*/
public function getStorage() {
return isset($this->storage) ? $this->storage : NULL;
}
/**
* {@inheritdoc}
*/
public function setStorage($storage) {
$this->storage = $storage;
return $this;
}
/**
* {@inheritdoc}
*/
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;
}
/**
* {@inheritdoc}
*/
public function getValues(array $fields) {
$values = [];
foreach ($fields as $field) {
if (isset($this->$field)) {
$values[$field] = $this->$field;
}
}
return $values;
}
/**
* {@inheritdoc}
*/
public function getLocations($check_only = FALSE) {
if (!isset($this->locations) && !$check_only) {
$this->locations = [];
foreach ($this->getStorage()->getLocations(['sid' => $this->getId()]) as $location) {
$this->locations[$location->type][$location->name] = $location->lid;
}
}
return isset($this->locations) ? $this->locations : [];
}
/**
* {@inheritdoc}
*/
public function addLocation($type, $name) {
$this->locations[$type][$name] = TRUE;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasLocation($type, $name) {
$locations = $this->getLocations();
return isset($locations[$type]) ? !empty($locations[$type][$name]) : FALSE;
}
/**
* {@inheritdoc}
*/
public function save() {
if ($storage = $this->getStorage()) {
$storage->save($this);
}
else {
throw new StringStorageException('The string cannot be saved because its not bound to a storage: ' . $this->getString());
}
return $this;
}
/**
* {@inheritdoc}
*/
public function delete() {
if (!$this->isNew()) {
if ($storage = $this->getStorage()) {
$storage->delete($this);
}
else {
throw new StringStorageException('The string cannot be deleted because its not bound to a storage: ' . $this->getString());
}
}
return $this;
}
}

View file

@ -0,0 +1,541 @@
<?php
namespace Drupal\locale;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition;
/**
* 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 = [];
/**
* 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 = []) {
$this->connection = $connection;
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public function getStrings(array $conditions = [], array $options = []) {
return $this->dbStringLoad($conditions, $options, 'Drupal\locale\SourceString');
}
/**
* {@inheritdoc}
*/
public function getTranslations(array $conditions = [], array $options = []) {
return $this->dbStringLoad($conditions, ['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, ['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 = []) {
$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', ['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(['sid' => $string->getId(), 'type' => $type, 'name' => $name])
->fields(['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(['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('The string cannot be deleted because it lacks some key fields: ' . $string->getString());
}
return $this;
}
/**
* {@inheritdoc}
*/
public function deleteStrings($conditions) {
$lids = $this->dbStringSelect($conditions, ['fields' => ['lid']])->execute()->fetchCol();
if ($lids) {
$this->dbDelete('locales_target', ['lid' => $lids])->execute();
$this->dbDelete('locales_source', ['lid' => $lids])->execute();
$this->dbDelete('locales_location', ['sid' => $lids])->execute();
}
}
/**
* {@inheritdoc}
*/
public function deleteTranslations($conditions) {
$this->dbDelete('locales_target', $conditions)->execute();
}
/**
* {@inheritdoc}
*/
public function createString($values = []) {
return new SourceString($values + ['storage' => $this]);
}
/**
* {@inheritdoc}
*/
public function createTranslation($values = []) {
return new TranslationString($values + [
'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, ['language', 'translation', 'customized'])) {
return 't';
}
elseif (in_array($field, ['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 = ['lid'];
}
elseif ($string->isTranslation()) {
$keys = ['lid', 'language'];
}
if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
return $values;
}
else {
return [];
}
}
/**
* 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 = [];
$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 = []) {
// 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", [
':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', ['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', ['sid']);
foreach (['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((new Condition('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 = new Condition('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(['context' => '', 'version' => 'none'], FALSE);
$fields = $string->getValues(['source', 'context', 'version']);
}
elseif ($string->isTranslation()) {
$string->setValues(['customized' => 0], FALSE);
$fields = $string->getValues(['lid', 'language', 'translation', 'customized']);
}
if (!empty($fields)) {
return $this->connection->insert($this->dbStringTable($string), $this->options)
->fields($fields)
->execute();
}
else {
throw new StringStorageException('The string cannot be saved: ' . $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(['source', 'context', 'version']);
}
elseif ($string->isTranslation()) {
$values = $string->getValues(['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('The string cannot be updated: ' . $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 = []) {
return $this->connection->query($query, $args, $this->options);
}
}

View file

@ -0,0 +1,216 @@
<?php
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,8 @@
<?php
namespace Drupal\locale;
/**
* Defines an exception thrown when storage operations fail.
*/
class StringStorageException extends \Exception {}

View file

@ -0,0 +1,180 @@
<?php
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 $options = []);
/**
* 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 $options = []);
/**
* 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 = []);
/**
* 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 = []);
/**
* 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 = []);
}

View file

@ -0,0 +1,123 @@
<?php
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;
/**
* {@inheritdoc}
*/
public function __construct($values = []) {
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;
}
/**
* {@inheritdoc}
*/
public function isSource() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function isTranslation() {
return !empty($this->lid) && !empty($this->language) && isset($this->translation);
}
/**
* {@inheritdoc}
*/
public function getString() {
return isset($this->translation) ? $this->translation : '';
}
/**
* {@inheritdoc}
*/
public function setString($string) {
$this->translation = $string;
return $this;
}
/**
* {@inheritdoc}
*/
public function isNew() {
return $this->isNew;
}
/**
* {@inheritdoc}
*/
public function save() {
parent::save();
$this->isNew = FALSE;
return $this;
}
/**
* {@inheritdoc}
*/
public function delete() {
parent::delete();
$this->isNew = TRUE;
return $this;
}
}