Thursday, January 14, 2010

Proposal for DNN support of RTL layout Part 1

DotNetNuke or (DNN in short) is an open source CMS that is built over ASP.Net. It is also extensible using modules providing a powerful programming platform. Unfortunately it has some issues with right to left (RTL) layout. In this blog post I am proposing the steps to be done in DNN to fully support RTL and LTR in the same portal out of the box.

For any CMS to fully support localization it needs:

  1. Static content localization: currently supported by DNN using Resx files and allows admins to modify them using the languages page in admin menu. admins can translate the resx files manually or just use a language pack if they can find a good and complete one.
  2. Dynamic content localization: DNN Corp is currently working on it. They put an infrastructure for dynamic content localization in version 5.2 but still not usable for the end-user. If you cannot wait for it to be complete you can use for example Magic content instead of HTML module, Ealo package for menu, breadcrumb localization and NBStore instead of DNN Store module. All these modules support dynamic content localization
  3. Right to left layout: for scripts that is written in right to left direction. Currently there is no official support for RTL in DNN. You will have to manually add in portal.css html {direction:rtl;} to make layout flipped. Most of the forms of DNN works good but some forms like login and manage profile don’t have a correct layout along with the default DNN menu in the default skin. Though the layout issue is not a blocking bug, it makes the layout looks wrong and amateurish for the end user.

The scripts that uses right to left layout are:

  1. Arabic script: which is used for writing Arabic, Persian, Urdu, Punjabi, Sindhi, etc.
  2. Hebrew script: which is used for writing Hebrew and Jewish languages like Yiddish, Ladino, etc.
  3. Scripts that are used by minority languages that nobody knows or cares about: like Thaana, Syriac, N'Ko, etc.
Only Arabic and Hebrew scripts have economic value especially in the Persian Gulf area and in Israel.

Root causes of messing up the RTL layout in web applications:

  1. In table cell alignment when a cell content is aligned left it remains aligned left event in RTL direction and when aligned right it remains aligned right in RTL direction. It should be mirrored. So when in a perfect world when i say when in RTL mode an html element is having

    <table align="left">


    it should align right and vice versa. unfortunately this is not the case.
  2. When using float left and float right css styles, they behave like point 1
  3. When assuming that images are displayed in specific order horizontally, this order is reversed in RTL mode. So some images will need to be flipped to look as good as in LTR mode (will give an example for this when discussing image RTL localization).
  4. The absolute positioning always starts from top left of the page regardless of the direction of the page (RTL or LTR). Usually this affects absolute positioning when an html element is placed using calculated co-ordinates to align left with another element. In RTL mode you should update the calculation to align right(will give an example for this when discussing JavaScript RTL localization).

These four reasons made some great Javascript frameworks like ExtJs unusable in RTL layout without much customization.

Though this is out of topic, Microsoft did a really good job on supporting RTL layout out of the box in  Windows forms. When a control is positioned using a left anchor in RTL layout it is positioned right though the anchor is still left. All of the standard windows forms controls behaves well in RTL mode. (though not all third party windows forms controls support RTL mode like Devexpress for example but this is not the time to discuss this here)

Straightforward steps to make DNN portal have an RTL layout:

  1. modify portal.css and add

html {direction:rtl;}


  1. Open ~/DesktopModules/AuthenticationServices/DNN/login.ascx and modify all table align="left" to align="right" and vice versa.
Unfortunately the straight forward approach fails due to:
  1. Usually my customers want a multi lingual portal (English/Arabic) using dynamic content localization as described above. Adding the direction:rtl in portal.css will lead to displaying English language in RTL mode which is not correct.
  2. The default menu is still in LTR mode. 
  3. The manage profile tab is messed up because of hardcoded style of text-align: left in Profile.ascx. (though the RTL issue is supposed to be resolved but apparently the developer that fixed it doesn’t know how RTL languages would look like because he left behind text-align: left which messed up the RTL layout)

So now we have a dead end for a trivial quick solution.

The good solution for the RTL problem:

Requirements:
  • The solution should fix the problem of RTL without messing the layout of the LTR languages on the same multi-lingual portal.
Detecting RTL culture server side:

We need to detect the RTL language culture server side to do some actions for handling RTL layout as I will discuss later. The best way to do this is using Thread.CurrentThread.CurrentUICulture.TextInfo.IsRightToLeft property which is deeply buried in the properties of the properties of the properties of the current thread object.

I suggest adding a shared/static readonly property to DotNetNuke.Services.Localization.Localization class to return the IsRightToLeft property

Public Shared ReadOnly Property IsRightToLeft() As Boolean
    Get
        Return Thread.CurrentThread.CurrentUICulture.TextInfo.IsRightToLeft
    End Get
End Property


Handling the embedded styles in tags:

After digging into the DNN code, I found an enumeration called DotNetNuke.UI.WebControls.LabelMode. this enumeration is used by the FieldEditorControl to display the register and the user profile control. Labels have the LabelMode left and controls have the LabelMode right. And then the FieldEditorControl calls the ToSring().ToLower() for the enumeration to get the text to embed in style left or right (these are the two properties we care about now for RTL layout). So the good news is that if we override the ToString() of the enumeration to render left property if not RTL then "left" else "right" and vice versa, all the forms that uses this control will be adjusted automatically without handling them case by case. The bad news is that there is no support for overriding ToString() method for enumerations :). So we will need an extension method to get label mode text and get the correct string according to text layout and this extension method should be used instead of ToString()

Public Module LabelModeHelper
       <Runtime.CompilerServices.Extension()> _
       Public Function GetLabelModeString(ByVal mode As LabelMode) As String
           If mode <> LabelMode.Left AndAlso mode <> LabelMode.Right Then Return mode.ToString().ToLower()
           If mode = LabelMode.Left Then
               Return If(Localization.IsRightToLeft, "right", "left")
           End If
           If mode = LabelMode.Right Then
               Return If(Localization.IsRightToLeft, "left", "right")
           End If
           Return String.Empty
       End Function
   End Module

There remains another issue; handling LabelMode in RTL is fixed in three places Membership, Profile and User user controls. the same code is copy pasted 3 times in the user control. I write it here for convenience

Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Init
           'Get the base Page
            Dim basePage As PageBase = TryCast(Me.Page, PageBase)
            If basePage IsNot Nothing Then
                'Check if culture is RTL
                If basePage.PageCulture.TextInfo.IsRightToLeft Then
                    UserEditor.LabelMode = LabelMode.Right
                Else
                    UserEditor.LabelMode = LabelMode.Left
                End If
            End If
        End Sub

The code above is not based on a good design as it was written 3 times over and over again!!!! I commented it to make my code work as it would conflict with it. In my code I am trying to imitate the perfect case of Windows forms as I mentioned above. Thus developers would LabelMode.Left without bothering to think how it should be rendered in RTL mode.

For styles embedded in ascx align="left" or text-align:left, and also for right all these makes RTL layout looks odd. So all these should be deleted except if really needed and use the following code <%= LabelMode.Left.GetLabelModeString %> for hard coded part of "left" or move the embded alignments like float, text-align to CSS files which I will discuss now how to localize them.

Handling CSS in RTL mode:

The CSS files would contain values of float:left , float:right, text-align:left, etc. These values should be reversed in RTL mode. The solution I propose here is to immitate ScriptManager control in ASP.Net for loading javascript files. For example when ScriptManager loads x.js file, if ASP.Net is in debug mode and there exists a file called x.debug.js, this file will be loaded instead of x.js which would be loaded in release mode.

I suggest that all css would be optimized for LTR mode for example a file called x.css and if there is style in it that would make RTL looks odd, it would be overridden in another CSS file with the same name for example x.rtl.css  with the same classes but with only the parts that  make a conflict overridden and loaded only when the language is detected to have an RTL layout. So in this case of RTL language both files would be loaded unlike what the ScriptManager does.

To achieve this we need to modify the code in the AddStyleSheet method in CDefault class in the core DNN library to check if:

  1. An RTL language is used for current culture
  2. and there exists a file with the same name of the CSS file being added to the page ending with .rtl.css

if both conditions are fulfilled then the .rtl.css class is loaded immediately after the file being added.

Public Sub AddStyleSheet(ByVal id As String, ByVal href As String, ByVal isFirst As Boolean)
    'Find the placeholder control
    Dim objCSS As Control = Me.FindControl("CSS")

    If Not objCSS Is Nothing Then
        'First see if we have already added the <LINK> control
        Dim objCtrl As Control = Page.Header.FindControl(id)

        If objCtrl Is Nothing Then
            Dim objLink As New HtmlLink()
            objLink.ID = id
            objLink.Attributes("rel") = "stylesheet"
            objLink.Attributes("type") = "text/css"
            objLink.Href = href

            If isFirst Then
                'Find the first HtmlLink
                Dim iLink As Integer
                For iLink = 0 To objCSS.Controls.Count - 1
                    If TypeOf objCSS.Controls(iLink) Is HtmlLink Then
                        Exit For
                    End If
                Next
                objCSS.Controls.AddAt(iLink, objLink)
                If Localization.IsRightToLeft Then
                    AddRTLStyleSheet(id, href, objCSS, iLink + 1)
                End If
            Else
                objCSS.Controls.Add(objLink)
                If Localization.IsRightToLeft Then
                    AddRTLStyleSheet(id, href, objCSS)
                End If
            End If
        End If
    End If
End Sub

Private Sub AddRTLStyleSheet(ByVal id As String, ByVal href As String, ByVal objCSS As Control, Optional ByVal index As Integer = -1)
    Dim rtlhref As String = GetRTLStyleSheetName(href)
    If String.IsNullOrEmpty(rtlhref) Then Return
    Dim objLink As New HtmlLink()
    objLink.ID = id + "rtl_"
    objLink.Attributes("rel") = "stylesheet"
    objLink.Attributes("type") = "text/css"
    objLink.Href = rtlhref
    If index <> -1 Then
        objCSS.Controls.AddAt(index, objLink)
    Else
        objCSS.Controls.Add(objLink)
    End If

End Sub

Private Function GetRTLStyleSheetName(ByVal href As String) As String
    Dim physicalFilePath As String = Server.MapPath(href)
    If Path.GetExtension(physicalFilePath).ToLower <> ".css" Then
        Return Nothing
    End If
    physicalFilePath = physicalFilePath.Substring(0, physicalFilePath.Length - 4) + ".rtl.css"
    If File.Exists(physicalFilePath) Then
        Return href.Substring(0, href.Length - 4) + ".rtl.css"
    End If
    Return Nothing
End Function

I have tested the code above and it loaded portal.rtl.css, skin.rtl.css and index.rtl.css which are files that I made. It may not be fully optimized but it is a start at least.

portal.rtl.css is the file that should contain html {direction:rtl;} not in portal.css.

The rtl css files should be only overriding properties in  css classes in the normal css files that are only related to rtl only. No need to copy/paste the stylesheet and make changes. This way you will double size of css and will have to maintain 2 css files. Below is an example for this.

In portal.css file there is class

.branding-bottom li { list-style: none; margin: 0 10px 0 0; padding: 0; display: block; width: 170px; float: left; }

In portal.rtl.css there should be

.branding-bottom li { float: right; margin: 0 0 0 10px; }

In my next post, I will discuss the 2 remaining aspects of RTL localization; localizing images and JavaScript files. I will publish my modified code as a proof of concept but won’t be suitable for production.

I hope that DNN corp incorporate these changes to make DNN more powerful than it already is.

Acknowledgements: I copied the style of the grey code boxes form here

2 comments:

  1. Thanks a lot for this Michael, its a great help

    ReplyDelete
  2. dear michael,

    i installed dslocalizer for english and arabic version of dnn 5.6.2 website.everthing is ok with this only the problem is RTL problem.

    steps i did for this
    -------------------------
    1.i have a skin file in skin_eng in
    C:\Documents and Settings\Administrator\Desktop\julie\DNN0408\Portals\0\Skins

    i ook a copy of skin_eng and renamed as skin_arb in this same folder.so now i have two skin file such as skin_eng and skin_arb

    2.for arabic tab i assigned skin_arb as skin.



    here i want your help
    1.what all files i need to edit for this RTL
    2.what all changes i need to do in skin.css file in skin_ar?

    if possible please help me..

    ReplyDelete