Master Language Switcher for Episerver
In this blog post, we are going to explain about some modifications we did to a tool we found useful created by Adam Najmanowicz, all credit attributed to him, called advanced language manipulation tool for Episerver (repository link) which allows you to change the master language for a site if your site has two or more languages. Unfortunately to make it work you must execute the script switchLang.sql in the database before being able to use which gets a little bit problematic if you are using DXP because you do not have direct access to the database for the pre production and production environments. Of course you can create a support ticket for that, but why bother. So, what we did to circumvent this issue is as follows.
First, we copied the ASP.NET web form file from the tool repository to our site. The code for the web form is the following. Pay special atention to the Inherits property in the first line, which should match with the code behind name space used in the cs file.
<%@ Page Language="c#" CodeBehind="LanguageTool.aspx.cs" AutoEventWireup="False" Inherits="Foundation.Tools.LanguageTool" Title="" %>
<%@ Register TagPrefix="EPiServerControls" Assembly="EPiServer.Cms.AspNet" Namespace="EPiServer.Web.WebControls" %>
<%@ Register TagPrefix="EpiserverProperties" Assembly="EPiServer.Cms.AspNet" Namespace="EPiServer.Web.PropertyControls" %>
<%@ Register TagPrefix="EPiServerUI" Assembly="EPiServer.UI" Namespace="EPiServer.UI.WebControls" %>
<div class="epi-contentContainer epi-padding">
<div class="epi-contentArea epi-paddingHorizontal">
<h1 class="EP-prefix">Master Language Switcher</h1>
<div class="epi-paddingVertical">
<form id="form" runat="server" class="epi-formArea">
<fieldset>
<legend>Styling form</legend>
<div class="epi-size10">
Page to process:
<EPiServerControls:InputPageReference runat="Server" ID="PageSelector" DisableCurrentPageOption="true" CssClass="epi-size3" />
</div>
<div class="epi-size10">
Target Language:
<EpiserverProperties:PropertyLanguageControl runat="server" ID="InputLanguage2" CssClass="epi-size15" />
<asp:PlaceHolder runat="server" ID="plhLanguageLontrol"></asp:PlaceHolder>
</div>
<div class="epi-size10">
Switch mode:
<asp:radiobuttonlist runat="server" id="rbgLanguageSwitchType" cssclass="epi-size15">
<asp:ListItem Text="Just switch between existing Language branches making the new one a master language" Selected="True" Value="1"></asp:ListItem>
<asp:ListItem Text="Convert the current master branch to new Language - the target language may not exist yet." Selected="False" Value="0"></asp:ListItem>
</asp:radiobuttonlist>
</div>
<div class="epi-size10">
Process children:
<asp:checkbox runat="Server" id="ckbRecursiveReplace" text="Select this option to perform the operation on the page as well as all of its children recursively." css="epi-size15" />
</div>
</fieldset>
<table>
<caption>Languages selector table</caption>
<tr>
<th scope="col">Change Language</th>
</tr>
<tr>
<td colspan="2">
<asp:button id="btnChangeLanguage" runat="server" text="Change Language" onclick="btnChangeLanguage_Click" cssclass="epi-cmsButton-text epi-cmsButton-tools" />
</td>
</tr>
</table>
</form>
</div>
<p></p>
<table>
<caption>Results table</caption>
<tr>
<th scope="col">Result message</th>
</tr>
<tr>
<td>
<asp:Literal ID="litMessage" Text="" runat="server" EnableViewState="false" />
</td>
</tr>
</table>
</div>
</div>
The cs file for this web form is where all the changes happened. We decided to add an initialization method called VerifyStoreProceduresHaveBeenCreated which will run the script switchLang.sql directly in code. We know, we know, it is not pretty and if somehow the CMS database schema changes this will probably not work anymore, but keep this as an example of how do to it. Apart from that there is no much difference between the original and this implementation.
using EPiServer;
using EPiServer.PlugIn;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.SpecializedProperties;
using Foundation.Cms.Services;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
namespace Foundation.Tools
{
[GuiPlugIn(DisplayName = "Advanced Language Tool",
Description = "Advanced Language Tool",
Area = PlugInArea.AdminMenu,
Url = "~/Tools/LanguageTool.aspx")]
public partial class LanguageTool : System.Web.UI.Page
{
private const string _spGetContentHierarchy = "cogGetContentHierarchy";
private const string _spGetContentBlocks = "cogGetContentBlocks";
private const string _spChangePageBranchMasterLanguage = "cogChangePageBranchMasterLanguage";
private readonly LogService _logService;
public LanguageTool()
{
_logService = new LogService();
}
protected override void OnInit(EventArgs e)
{
if (!PrincipalInfo.HasAdminAccess)
{
Response.Redirect("/", true);
}
base.OnInit(e);
InputLanguage2.PropertyData = new PropertyLanguage();
InputLanguage2.CreateEditControls();
VerifyStoreProceduresHaveBeenCreated();
}
private void VerifyStoreProceduresHaveBeenCreated()
{
CreateCogChangePageBranchMasterLanguage();
CreateCogGetContentBlocks();
CreateCogGetContentHierarchy();
}
private void CreateCogChangePageBranchMasterLanguage()
{
ExecuteScript(
"CREATE PROCEDURE[dbo].[cogChangePageBranchMasterLanguage] " +
" " +
" @page_id int, " +
" @language_branch varchar(20), " +
" @recursive bit, " +
" @switch_only bit " +
"AS " +
" " +
"DECLARE @language_branch_id nchar(17); " +
" DECLARE @language_branch_nid int; " +
" DECLARE @prev_language_branch_nid int; " +
" DECLARE @child_Id int; " +
" DECLARE @Fetch int; " +
" DECLARE @target_lang_version_exist int; " +
" " +
" SET @language_branch_nid = (SELECT pkID FROM tblLanguageBranch WHERE(LanguageID = @language_branch)) " +
"SET @language_branch_id = (SELECT LanguageId FROM tblLanguageBranch WHERE(LanguageID = @language_branch)) " +
"SET @prev_language_branch_nid = (SELECT fkMasterLanguageBranchID FROM tblContent WHERE pkId = @page_id) " +
"SET @target_lang_version_exist = (SELECT count(*) FROM tblContentLanguage WHERE(fkContentID = @page_id AND fkLanguageBranchID = @language_branch_nid)) " +
" " +
" " +
"IF 1 = @switch_only " +
" BEGIN " +
" print 'target_lang_version_exist' + str(@target_lang_version_exist); " +
" IF @target_lang_version_exist > 0 " +
" " +
" BEGIN " +
" UPDATE tblContent " +
" SET fkMasterLanguageBranchID = @language_branch_nid " +
" " +
" WHERE pkID = @page_id AND fkMasterLanguageBranchID = @prev_language_branch_nid " +
" " +
" END " +
" ELSE " +
" " +
" BEGIN " +
" " +
" RAISERROR " +
" (N'The Selected page with ID:%d, cannot switch master branch since there is no version in the selected target language: %s.', " +
" 11, 1, @page_id, @language_branch); " +
" END " +
" END " +
"ELSE " +
" BEGIN " +
" " +
" IF @target_lang_version_exist > 0 " +
" " +
" BEGIN " +
" " +
" RAISERROR " +
" (N'The Selected page with ID:%d, cannot be translated since there already is a version in the selected target language: %s.', " +
" 11, 1, @page_id, @language_branch); " +
" END " +
" ELSE " +
" " +
" BEGIN " +
" UPDATE tblContent " +
" SET " +
" " +
" fkMasterLanguageBranchID = @language_branch_nid " +
" " +
" WHERE pkId = @page_id " +
" " +
" " +
" UPDATE tblContentProperty " +
" " +
" SET fkLanguageBranchID = @language_branch_nid " +
" " +
" WHERE fkContentID = @page_id AND fkLanguageBranchID = @prev_language_branch_nid " +
" " +
" " +
" UPDATE tblContentLanguage " +
" " +
" SET fkLanguageBranchID = @language_branch_nid " +
" " +
" WHERE fkContentID = @page_id AND fkLanguageBranchID = @prev_language_branch_nid " +
" " +
" " +
" UPDATE tblWorkContent " +
" " +
" SET fkLanguageBranchID = @language_branch_nid " +
" " +
" WHERE fkContentID = @page_id AND fkLanguageBranchID = @prev_language_branch_nid " +
" " +
" END " +
" END " +
" " +
"IF 1 = @recursive " +
"BEGIN " +
" DECLARE children_cursor CURSOR LOCAL FOR " +
" " +
" select pkId from tblContent where fkParentId = @page_id " +
" " +
" " +
" OPEN children_cursor " +
" " +
" " +
" FETCH NEXT FROM children_cursor INTO @child_Id " +
" " +
" SET @Fetch =@@FETCH_STATUS " +
" " +
" WHILE @Fetch = 0 " +
" " +
" BEGIN " +
" print @child_id " +
" print @language_branch_id " +
" exec[dbo].[cogChangePageBranchMasterLanguage] @child_id, @language_branch_id, @recursive, @switch_only " +
" FETCH NEXT FROM children_cursor INTO @child_Id " +
" SET @Fetch =@@FETCH_STATUS " +
" END " +
" " +
" " +
" CLOSE children_cursor " +
" " +
" DEALLOCATE children_cursor " +
"END " +
" ");
}
private void CreateCogGetContentBlocks()
{
ExecuteScript(
" CREATE PROCEDURE[dbo].[cogGetContentBlocks] " +
" " +
"@page_id int " +
" AS " +
"BEGIN " +
" " +
" SET NOCOUNT ON; " +
" " +
"SELECT tblContent.pkID, tblContent.ContentType from tblContent " +
" INNER JOIN tblContentSoftlink ON tblContent.ContentGUID = tblContentSoftlink.fkReferencedContentGUID " +
"WHERE tblContentSoftlink.fkOwnerContentID = @page_id AND tblContent.ContentType = 1 " +
"ORDER BY tblContent.pkID " +
" END " +
//"GO " +
" ");
}
private void CreateCogGetContentHierarchy()
{
ExecuteScript(
"CREATE PROCEDURE [dbo].[cogGetContentHierarchy] " +
" @page_id int " +
" AS " +
" BEGIN " +
" " +
" " +
" SET NOCOUNT ON; " +
" " +
" WITH content " +
" AS( " +
" SELECT Parent.pkID, Parent.ContentGUID, Parent.ContentType, Parent.fkParentID " +
" FROM tblContent As Parent " +
" WHERE Parent.pkID = @page_id " +
" " +
" UNION ALL " +
" " +
" SELECT Child.pkID, Child.ContentGUID, Child.ContentType, Child.fkParentID " +
" FROM tblContent as Child " +
" INNER JOIN content " +
" ON Child.fkParentID = content.pkID " +
" WHERE Child.fkParentID IS NOT NULL) " +
" SELECT* " +
" FROM content " +
" ORDER BY content.pkID " +
" " +
" END " +
" ");
}
private void ExecuteScript(string script)
{
try
{
using SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["EPiServerDB"].ConnectionString);
SqlCommand command = new SqlCommand(script, connection) { CommandType = CommandType.Text };
command.Connection.Open();
command.ExecuteNonQuery();
}
catch (InvalidOperationException ex)
{
_logService.Error(ex.Message, ex);
}
catch (SqlException ex)
{
if (ex.Number != 2714)
{
_logService.Error(ex.Message, ex);
}
}
catch (ArgumentException ex)
{
_logService.Error(ex.Message, ex);
}
}
protected void btnChangeLanguage_Click(object sender, EventArgs e)
{
var contentCacheRemover = ServiceLocator.Current.GetInstance<IContentCacheRemover>();
InputLanguage2.ApplyEditChanges();
if (PageSelector.PageLink != null && !string.IsNullOrEmpty((string)InputLanguage2.PropertyData.Value))
{
try
{
RunSwitchLanguage(PageSelector.PageLink.ID, (string)InputLanguage2.PropertyData.Value, ckbRecursiveReplace.Checked, rbgLanguageSwitchType.SelectedIndex == 0);
RunSwitchBlockLanguage(PageSelector.PageLink.ID, (string)InputLanguage2.PropertyData.Value, ckbRecursiveReplace.Checked, rbgLanguageSwitchType.SelectedIndex == 0);
contentCacheRemover.Clear();
litMessage.Text = "Page language changed.";
return;
}
catch (Exception ex)
{
litMessage.Text = $"Page change failed: <br>{ex.Message}";
}
}
litMessage.Text = "Page language not changed. No page or language selected.";
}
public IEnumerable<int> GetContentHierarchy(int pageId)
{
List<int> ids = new List<int>();
using SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["EPiServerDB"].ConnectionString);
try
{
SqlCommand command =
new SqlCommand(_spGetContentHierarchy, connection) { CommandType = CommandType.StoredProcedure };
command.Parameters.Add("@page_id", SqlDbType.Int).Value = pageId;
command.Connection.Open();
var reader = command.ExecuteReader();
while (reader.Read())
{
ids.Add(int.Parse(reader["pkID"].ToString()));
}
}
catch (InvalidOperationException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
catch (SqlException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
catch (ArgumentException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
return ids;
}
public IEnumerable<int> GetContentBlocks(int pageId)
{
List<int> ids = new List<int>();
using SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["EPiServerDB"].ConnectionString);
try
{
SqlCommand command =
new SqlCommand(_spGetContentBlocks, connection) { CommandType = CommandType.StoredProcedure };
command.Parameters.Add("@page_id", SqlDbType.Int).Value = pageId;
command.Connection.Open();
command.ExecuteNonQuery();
var reader = command.ExecuteReader();
while (reader.Read())
{
ids.Add(int.Parse(reader["pkID"].ToString()));
}
}
catch (InvalidOperationException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
catch (SqlException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
catch (ArgumentException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
return ids;
}
public void RunSwitchBlockLanguage(int contentId, string langBranch, bool recursive, bool switchOnly)
{
List<int> ids = new List<int>();
var pageIds = GetContentHierarchy(contentId);
foreach (var pageId in pageIds)
{
var blockIds = GetContentBlocks(pageId);
ids.AddRange(blockIds);
}
foreach (var id in ids)
{
RunSwitchLanguage(id, (string)InputLanguage2.PropertyData.Value, ckbRecursiveReplace.Checked, rbgLanguageSwitchType.SelectedIndex == 0);
}
}
public bool RunSwitchLanguage(int contentId, string langBranch, bool recursive, bool switchOnly)
{
using SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["EPiServerDB"].ConnectionString);
try
{
SqlCommand command =
new SqlCommand(_spChangePageBranchMasterLanguage, connection)
{
CommandType = CommandType.StoredProcedure
};
command.Parameters.Add("@page_id", SqlDbType.Int).Value = contentId;
command.Parameters.Add("@language_branch", SqlDbType.VarChar, 20).Value = langBranch;
command.Parameters.Add("@recursive", SqlDbType.Bit).Value = recursive;
command.Parameters.Add("@switch_only", SqlDbType.Bit).Value = switchOnly;
command.Connection.Open();
command.ExecuteNonQuery();
return true;
}
catch (InvalidOperationException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
catch (SqlException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
catch (ArgumentException ex)
{
//log and/or rethrow or ignore
_logService.Error(ex.Message, ex);
}
return false;
}
}
}
You will probably also need the ASPX designer cs file, but you can also copy this file directly from the original repository
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Foundation.Tools {
public partial class LanguageTool {
/// <summary>
/// form control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::System.Web.UI.HtmlControls.HtmlForm form;
/// <summary>
/// PageSelector control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::EPiServer.Web.WebControls.InputPageReference PageSelector;
/// <summary>
/// InputLanguage2 control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::EPiServer.Web.PropertyControls.PropertyLanguageControl InputLanguage2;
/// <summary>
/// plhLanguageLontrol control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::System.Web.UI.WebControls.PlaceHolder plhLanguageLontrol;
/// <summary>
/// rbgLanguageSwitchType control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::System.Web.UI.WebControls.RadioButtonList rbgLanguageSwitchType;
/// <summary>
/// ckbRecursiveReplace control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::System.Web.UI.WebControls.CheckBox ckbRecursiveReplace;
/// <summary>
/// btnChangeLanguage control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::System.Web.UI.WebControls.Button btnChangeLanguage;
/// <summary>
/// litMessage control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::System.Web.UI.WebControls.Literal litMessage;
}
}
LogService class is our own implementation to write errors and info messages to the logs of the CMS, you can use your own implementation.
public class LogService
{
private readonly ILogger _logger = LogManager.GetLogger();
/// <summary>
/// Logs a debug string message.
/// </summary>
/// <param name="message">Message to log.</param>
public void Debug(string message)
{
_logger.Debug(message);
}
/// <summary>
/// Logs information.
/// </summary>
/// <param name="message">Message to log.</param>
public void Information(string message)
{
_logger.Information(message);
}
/// <summary>
/// Logs an error.
/// </summary>
/// <param name="message">Message to log.</param>
/// <param name="e">Error.</param>
public void Error(string message, Exception e = null)
{
var result = $@"[Error Message] : {message}";
if (e != null)
{
result +=
$@"[Exception Message]: {e.Message}
[Stack]: \r\n {e.StackTrace}";
}
_logger.Error(result);
}
/// <summary>
/// Logs an exception.
/// </summary>
/// <param name="e">Exception to log.</param>
public void Error(Exception e)
{
if (e != null)
{
var result =
$@"[Exception Message]: {e.Message}
[Stack]: \r\n {e.StackTrace}";
_logger.Error(result);
}
}
/// <summary>
/// Logs a warning.
/// </summary>
/// <param name="message">Message to log.</param>
public void Warning(string message)
{
_logger.Warning(message);
}
}
Now, if you run your site again. It will create a new entry in the admin section, admin tab, tools sub section called Advanced Language Tool
The tool will let you choose which page to process and then choose the language that you want to make it master. Then, you have two options, just switch master languages, if both languages exists, or convert current master branch to a new language (be careful when you use this option). Finally, you can choose if the change will apply only to the current page or the current page plus all its children. Press the change language button and if everything goes well or if there is an error you will see what happened in the results message section.
And that is it. Now, you can change the master language of any page or pages inside a site without difficulty. If you have any question let me know. I hope it will help someone and as always keep learning !!!
Leave a Reply