В SharePoint Server есть Variation Labels, есть Publishing Pages, а вот в Foundation ничего этого нет.
Я начал проработку локализации меню с двух неудачных попыток. Потратил огромную кучу времени (хотя как оказалось, всё можно сделать за полчаса), но с третьей попытки всё-таки мне удалось сделать всё как я хотел. А хотел я так:
- Пункты меню развертываются фичей. И удаляются тоже ею же, при деактивации. Фича развертывается в пределах веба.
- При переключении текущего языка на сайте, пункты меню сразу же локализуются. Т.е. сайт может смотреть одновременно несколько пользователей, и все будут видеть меню именно на выбранном ими языке.
Предыстория
Сначала я пытался подменить класс SPNavigationProvider. Но, есть два минуса:
- Некрасиво. В этом способе подразумевается, что вместо названий пунктов меню мы вбиваем строки "$Resources:File,Key", и соответственно эта хрень отображается в стандартном редакторе меню.
- При вызове /_layouts/viewlsts.aspx w3wp.exe зависал намертво, при этом на 100% забивая проц. И в логах - ни одной ошибки :( Причем, вся админка в то же самое время работала нормально.
Потом я пытался использовать другое решение, описанное на CodePlex'е. Вкратце, идея там состоит в том, чтобы определять в веб.конфиге экземпляр XmlSiteMapProvider, указывать ему в качестве источника sitemap-файл, и в masterpage добавлять делегат. Но это решение мне тоже не понравилось:
- У нас 4 мастер-страницы, на каждый вариант дизайна. В будущем будет больше. Никогда не любил ковыряться в этом чудовищном говнокоде.
- Самое главное, при использовании этого способа меню получается нередактируемым стандартными средствами SharePoint, т.к. прицеплено строго к sitemap-файлу. Ребята даже специально скрывают соответствующий пункт меню в админке через кастомэкшн.
- Более того, верхнее меню в админке, и на том же злополучном /_layouts/viewlsts.aspx оставались неизмененными.
При этом пропадала вторая проблема, но третий пункт оставался. Впрочем, его наверное тоже можно было решить, вытащив наше меню из плейсхолдера, а сам плэйсхолдер скрыв (Visible="False"). Хотя тоже ведь, не очень правильный вариант :(
Во всех этих вариантах еще добавлялись трудозатраты немаленькие, и риски, соответственно, тоже. А сроки, они как всегда - подпирают.
Так что я уж было отчаялся, и начал писать код для статического развертывания. Т.е., при активации фичи, в ресивере кодом добавляем нужные нам пункты в меню, сразу же локализуя их в локаль сайта. И on-fly локализация в этом случае, естественно, не работает. Но тут я заметил две вещи:
- Наличие поля TitleResource в классе SPNavigationNode
- Потом я еще заметил, что на простом сайте, там где после создания один единственный пункт меню ("Домашняя"), если переключиться на другой язык - этот пункт локализуется! Опытным путем было выяснено, что редактирование разных трансляций тоже происходит при переключении текущего языка.
Вот такой вот получился тернистый путь к звездам, блин :)
Рабочий вариант
Основной код лежит в Feature Receiver. На всякий случай, на пальцах, нужно добавить в проект фичу, нажать на неё правой мышой, и выбрать Add Event Receiver, добавится класс с несколькими закомментированными методами.
Дальше вставляем в этот класс вот такой код:
public override void FeatureActivated(SPFeatureReceiverProperties properties) { SPWeb web = properties.Feature.Parent as SPWeb; // Добавляем наши пункты в верхнее меню AddSiteMenuNodeRecursive(LocalizedSiteMenu.Instance.TopNavMenu, web.Navigation.TopNavigationBar); // Добавляем наши пункты в quicklaunch AddSiteMenuNodeRecursive(LocalizedSiteMenu.Instance.QuickLaunchMenu, web.Navigation.QuickLaunch); web.Update(); } private void AddSiteMenuNodeRecursive(IEnumerable<SiteMenuNode> nodes, SPNavigationNodeCollection menu) { foreach (SiteMenuNode node in nodes) { SPNavigationNode navigationNode = new SPNavigationNode(node.Title, node.Url); menu.AddAsLast(navigationNode); navigationNode.Update(); foreach (int lcid in LocalizationHelper.SupportedLocales) { navigationNode.TitleResource.SetValueForUICulture(new CultureInfo(lcid), SPUtility.GetLocalizedString(node.Title, "ResourceFile", (uint)lcid)); } if (node.HasChildNodes) { AddSiteMenuNodeRecursive(node.ChildNodes, navigationNode.Children); } navigationNode.Update(); } }При деактивации фичи надо заметать следы. Для этого, ищем наши пункты меню по их Url'у, и если находим - то удаляем.
public override void FeatureDeactivating(SPFeatureReceiverProperties properties) { SPWeb web = properties.Feature.Parent as SPWeb; // Удаляем наши пункты из верхнего меню foreach (SiteMenuNode node in LocalizedSiteMenu.Instance.TopNavMenu) { SPNavigationNode found = web.Navigation.TopNavigationBar.Navigation.GetNodeByUrl(node.Url); if (found != null) web.Navigation.TopNavigationBar.Delete(found); } // Удаляем наши пункты из quicklaunch foreach (SiteMenuNode node in LocalizedSiteMenu.Instance.QuickLaunchMenu) { SPNavigationNode found = web.Navigation.QuickLaunch.Navigation.GetNodeByUrl(node.Url); if (found != null) web.Navigation.QuickLaunch.Delete(found); } web.Update(); }Плюс к тому, еще нужно создать файлик с классом SiteMenuNode, который служит для хранения пунктов меню. Это простенький класс с тремя свойствами:
namespace SharePointTestProject.Localization { public class SiteMenuNode { public string Url { get; set; } public string Title { get; set; } public IEnumerable<SiteMenuNode> ChildNodes { get; set; } public bool HasChildNodes { get { return (ChildNodes != null && ChildNodes.Count() > 0); } } } }Наконец, надо еще определить собственно коллекцию пунктов меню, которая уже упоминалась выше. Для этого я создал синглтон и захардкодил эту самую коллекцию:
namespace SharePointTestProject.Localization { public class LocalizedSiteMenu { private static LocalizedSiteMenu instance = null; public static LocalizedSiteMenu Instance { get { if (instance == null) instance = new LocalizedSiteMenu(); return instance; } } public IEnumerable<SiteMenuNode> TopNavMenu = new SiteMenuNode[] { new SiteMenuNode() { Url = "/Pages/Page1.aspx", Title = "$Resources:ResourceFile,Page1Title" }, new SiteMenuNode() { Url = "/Pages/Page2.aspx", Title = "$Resources:ResourceFile,Page2Title" } }; public IEnumerable<SiteMenuNode> QuickLaunchMenu = new SiteMenuNode[] { new SiteMenuNode() { Url = "/Products/default.aspx", Title = "$Resources:ResourceFile,ProductsTitle", ChildNodes = new SiteMenuNode[] { new SiteMenuNode() { Url = "/Products/Product1.aspx", Title = "$Resources:ResourceFile,Product1Title" }, new SiteMenuNode() { Url = "/Products/Product2.aspx", Title = "$Resources:ResourceFile,Product2Title" } } }, new SiteMenuNode() { Url = "/Pages/Page3.aspx", Title = "$Resources:ResourceFile,Page3Title" } }; } }В этом примере задается верхнее меню из трех пунктов (оно будет добавлено в конец существующего), а также меню QuickLaunch из двух пунктов, один из которых является составным.
Вообще, Вы можете подгружать пункты откуда-нибудь динамически, к примеру, из какого-нибудь конфигурационного файла, хотя не уверен, что это нужно.
P.S. Код проверен, мин нет.
Спасибо! Очень полезный пост... но LocalizationHelper.SupportedLocales - непонятно что такое, вместо этого можно заюзать web.SupportedUICultures, и еще, почему-то в ресурс подставляется точка с запятой. Например: вместо $Resources:ResourceFile,Product2Title в конечном счете получится $Resources:ResourceFile,Product2Title; , а такого ресурса нет. Пока так и не понял почему так и как с этим бороться
ОтветитьУдалитьСори, просто оказалось, что ресурсы нужно в папку Resouces класть. (((
ОтветитьУдалитьПривет, Илья!
ОтветитьУдалитьLocalizationHelper - это специальный класс в нашем проекте, который предоставляет некоторые функции для локализации.
Сейчас, кстати, перешли на использование кодогенерации (т.о. напрямую этот класс уже не используется).
Что касается свойства этого класса SupportedLocales, то это просто список всех локалей, на которые наш портал переведен.
Выглядит он примерно так:
public string IEnumerable SupportedLocales
{
get
{
return new int[] { 1033, 1049 };
}
}
Отличная статья. Только вопрос по поводу файла ресурсов. Его необходимо создавать самому или можно использовать стандартные?
ОтветитьУдалитьОтличная статья, от себя можем добавить, если нужен профессиональный перевод от носителей языка и тестирование, то можно использовать данный сервис http://alconost.com/services/website-translation
ОтветитьУдалить