using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; namespace Umbraco.Core.Services.Implement { /// /// Represents the Localization Service, which is an easy access to operations involving and /// public class LocalizationService : ScopeRepositoryService, ILocalizationService { private readonly IDictionaryRepository _dictionaryRepository; private readonly ILanguageRepository _languageRepository; private readonly IAuditRepository _auditRepository; public LocalizationService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDictionaryRepository dictionaryRepository, IAuditRepository auditRepository, ILanguageRepository languageRepository) : base(provider, logger, eventMessagesFactory) { _dictionaryRepository = dictionaryRepository; _auditRepository = auditRepository; _languageRepository = languageRepository; } /// /// Adds or updates a translation for a dictionary item and language /// /// /// /// /// /// /// This does not save the item, that needs to be done explicitly /// public void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage language, string value) { if (item == null) throw new ArgumentNullException(nameof(item)); if (language == null) throw new ArgumentNullException(nameof(language)); var existing = item.Translations.FirstOrDefault(x => x.Language.Id == language.Id); if (existing != null) { existing.Value = value; } else { item.Translations = new List(item.Translations) { new DictionaryTranslation(language, value) }; } } /// /// Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified. /// /// /// /// /// public IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string defaultValue = null) { using (var scope = ScopeProvider.CreateScope()) { //validate the parent if (parentId.HasValue && parentId.Value != Guid.Empty) { var parent = GetDictionaryItemById(parentId.Value); if (parent == null) throw new ArgumentException($"No parent dictionary item was found with id {parentId.Value}."); } var item = new DictionaryItem(parentId, key); if (defaultValue.IsNullOrWhiteSpace() == false) { var langs = GetAllLanguages(); var translations = langs.Select(language => new DictionaryTranslation(language, defaultValue)) .Cast() .ToList(); item.Translations = translations; } var saveEventArgs = new SaveEventArgs(item); if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, saveEventArgs)) { scope.Complete(); return item; } _dictionaryRepository.Save(item); // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(item); saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedDictionaryItem, this, saveEventArgs); scope.Complete(); return item; } } /// /// Gets a by its id /// /// Id of the /// public IDictionaryItem GetDictionaryItemById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var item = _dictionaryRepository.Get(id); //ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(item); return item; } } /// /// Gets a by its id /// /// Id of the /// public IDictionaryItem GetDictionaryItemById(Guid id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var item = _dictionaryRepository.Get(id); //ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(item); return item; } } /// /// Gets a by its key /// /// Key of the /// public IDictionaryItem GetDictionaryItemByKey(string key) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var item = _dictionaryRepository.Get(key); //ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(item); return item; } } /// /// Gets a list of children for a /// /// Id of the parent /// An enumerable list of objects public IEnumerable GetDictionaryItemChildren(Guid parentId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.ParentId == parentId); var items = _dictionaryRepository.Get(query).ToArray(); //ensure the lazy Language callback is assigned foreach (var item in items) EnsureDictionaryItemLanguageCallback(item); return items; } } /// /// Gets a list of descendants for a /// /// Id of the parent, null will return all dictionary items /// An enumerable list of objects public IEnumerable GetDictionaryItemDescendants(Guid? parentId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var items = _dictionaryRepository.GetDictionaryItemDescendants(parentId).ToArray(); //ensure the lazy Language callback is assigned foreach (var item in items) EnsureDictionaryItemLanguageCallback(item); return items; } } /// /// Gets the root/top objects /// /// An enumerable list of objects public IEnumerable GetRootDictionaryItems() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.ParentId == null); var items = _dictionaryRepository.Get(query).ToArray(); //ensure the lazy Language callback is assigned foreach (var item in items) EnsureDictionaryItemLanguageCallback(item); return items; } } /// /// Checks if a with given key exists /// /// Key of the /// True if a exists, otherwise false public bool DictionaryItemExists(string key) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var item = _dictionaryRepository.Get(key); return item != null; } } /// /// Saves a object /// /// to save /// Optional id of the user saving the dictionary item public void Save(IDictionaryItem dictionaryItem, int userId = 0) { using (var scope = ScopeProvider.CreateScope()) { if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, new SaveEventArgs(dictionaryItem))) { scope.Complete(); return; } _dictionaryRepository.Save(dictionaryItem); // ensure the lazy Language callback is assigned // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(dictionaryItem); scope.Events.Dispatch(SavedDictionaryItem, this, new SaveEventArgs(dictionaryItem, false)); Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); } } /// /// Deletes a object and its related translations /// as well as its children. /// /// to delete /// Optional id of the user deleting the dictionary item public void Delete(IDictionaryItem dictionaryItem, int userId = 0) { using (var scope = ScopeProvider.CreateScope()) { var deleteEventArgs = new DeleteEventArgs(dictionaryItem); if (scope.Events.DispatchCancelable(DeletingDictionaryItem, this, deleteEventArgs)) { scope.Complete(); return; } _dictionaryRepository.Delete(dictionaryItem); deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedDictionaryItem, this, deleteEventArgs); Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); } } /// /// Gets a by its id /// /// Id of the /// public ILanguage GetLanguageById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _languageRepository.Get(id); } } /// /// Gets a by its iso code /// /// Iso Code of the language (ie. en-US) /// public ILanguage GetLanguageByIsoCode(string isoCode) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _languageRepository.GetByIsoCode(isoCode); } } /// public int? GetLanguageIdByIsoCode(string isoCode) { using (ScopeProvider.CreateScope(autoComplete: true)) { return _languageRepository.GetIdByIsoCode(isoCode); } } /// public string GetLanguageIsoCodeById(int id) { using (ScopeProvider.CreateScope(autoComplete: true)) { return _languageRepository.GetIsoCodeById(id); } } /// public string GetDefaultLanguageIsoCode() { using (ScopeProvider.CreateScope(autoComplete: true)) { return _languageRepository.GetDefaultIsoCode(); } } /// public int? GetDefaultLanguageId() { using (ScopeProvider.CreateScope(autoComplete: true)) { return _languageRepository.GetDefaultId(); } } /// /// Gets all available languages /// /// An enumerable list of objects public IEnumerable GetAllLanguages() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _languageRepository.GetMany(); } } /// /// Saves a object /// /// to save /// Optional id of the user saving the language public void Save(ILanguage language, int userId = 0) { using (var scope = ScopeProvider.CreateScope()) { // write-lock languages to guard against race conds when dealing with default language scope.WriteLock(Constants.Locks.Languages); // look for cycles - within write-lock if (language.FallbackLanguageId.HasValue) { var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x); if (!languages.ContainsKey(language.FallbackLanguageId.Value)) throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id."); if (CreatesCycle(language, languages)) throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle."); } var saveEventArgs = new SaveEventArgs(language); if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs)) { scope.Complete(); return; } _languageRepository.Save(language); saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedLanguage, this, saveEventArgs); Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); scope.Complete(); } } private bool CreatesCycle(ILanguage language, IDictionary languages) { // a new language is not referenced yet, so cannot be part of a cycle if (!language.HasIdentity) return false; var id = language.FallbackLanguageId; while (true) // assuming languages does not already contains a cycle, this must end { if (!id.HasValue) return false; // no fallback means no cycle if (id.Value == language.Id) return true; // back to language = cycle! id = languages[id.Value].FallbackLanguageId; // else keep chaining } } /// /// Deletes a by removing it (but not its usages) from the db /// /// to delete /// Optional id of the user deleting the language public void Delete(ILanguage language, int userId = 0) { using (var scope = ScopeProvider.CreateScope()) { // write-lock languages to guard against race conds when dealing with default language scope.WriteLock(Constants.Locks.Languages); var deleteEventArgs = new DeleteEventArgs(language); if (scope.Events.DispatchCancelable(DeletingLanguage, this, deleteEventArgs)) { scope.Complete(); return; } // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted _languageRepository.Delete(language); deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedLanguage, this, deleteEventArgs); Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); scope.Complete(); } } private void Audit(AuditType type, string message, int userId, int objectId, string entityType) { _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, message)); } /// /// This is here to take care of a hack - the DictionaryTranslation model contains an ILanguage reference which we don't want but /// we cannot remove it because it would be a large breaking change, so we need to make sure it's resolved lazily. This is because /// if developers have a lot of dictionary items and translations, the caching and cloning size gets much much larger because of /// the large object graphs. So now we don't cache or clone the attached ILanguage /// private void EnsureDictionaryItemLanguageCallback(IDictionaryItem d) { var item = d as DictionaryItem; if (item == null) return; item.GetLanguage = GetLanguageById; foreach (var trans in item.Translations.OfType()) trans.GetLanguage = GetLanguageById; } public Dictionary GetDictionaryItemKeyMap() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _dictionaryRepository.GetDictionaryItemKeyMap(); } } #region Events /// /// Occurs before Delete /// public static event TypedEventHandler> DeletingLanguage; /// /// Occurs after Delete /// public static event TypedEventHandler> DeletedLanguage; /// /// Occurs before Delete /// public static event TypedEventHandler> DeletingDictionaryItem; /// /// Occurs after Delete /// public static event TypedEventHandler> DeletedDictionaryItem; /// /// Occurs before Save /// public static event TypedEventHandler> SavingDictionaryItem; /// /// Occurs after Save /// public static event TypedEventHandler> SavedDictionaryItem; /// /// Occurs before Save /// public static event TypedEventHandler> SavingLanguage; /// /// Occurs after Save /// public static event TypedEventHandler> SavedLanguage; #endregion } }