Asp.Net Localize Utility
Convert pages to use explicit localizations (as opposed to the implicit localizations with Visual Studio). Creates either App_GlobalResources, App_LocalResources or SQL scripts (for sql localization provider).
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
using System.Xml.XPath;
namespace AddAspResources
{
/// <summary>
/// Converts a page to use explicit asp resources (similar to Visual Studio Tools>Localize)
/// </summary>
/// <remarks>
/// Important: text must be localizable first (eg asp:Label, not in html p tags).
/// It can be run repeatedly (it recognises already localized text) but it doesn't load the old localizations.
/// <para>Re-running</para>
/// <para>For resx (global and local) the existing files are read so there are non duplicate keys.</para>
/// <para>For sql providers, this is not done, so duplicate keys are possible (especially in global mode)</para>
/// </remarks>
/// <example>
/// <code>
/// //Called from a non-web project
/// Localize c = new Localize();
/// c.BackUpSavedFiles = false; //by default it saves a backup of each file
/// c.ResourceStore = Localize.ResourceStores.App_GlobalResources; //can be global or local, or to sql scripts
/// c.ConvertDirectory(@"C:\Users\x\Desktop\Documents\Visual Studio 2005\WebSites\LocalizeMe"); //convert a directory
/// </code>
/// <code>
/// //called from a web project- complete website - put a button on any page and insert this...
/// Localize c = new Localize();
/// c.ConvertDirectory(); //convert everything
/// </code>
/// <code>
/// //called from a web project- single page - put a button on any page and insert this...
/// Localize c = new Localize();
/// //by default this is local resx
/// c.ConvertFile(Server.MapPath(Request.AppRelativeCurrentExecutionFilePath)); //just this file
/// </code>
/// <code>
/// //called from a web project- sitemap only - put a button on any page and insert this...
/// Localize c = new Localize();
/// c.ResourceStore = Localize.ResourceStores.App_GlobalResources;
/// c.ConvertSiteMap();
/// c.SaveResources(); //you only have to do this for global resources- if local, it's already done
/// </code>
/// </example>
public class Localize
{
#region ResourceStores enum
/// <summary>
/// Where resources are stored- global or local resx files, or in a sql script for a database
/// </summary>
public enum ResourceStores
{
/// <summary>
/// Creates resx files in App_LocalResources for each directory. Sitemaps are always global resources. DEFAULT.
/// </summary>
App_LocalResources,
/// <summary>
/// Creates resx files in App_GlobalResources in the root (or at least initial directory)
/// </summary>
App_GlobalResources,
/// <summary>
/// WestWind sql provider. Creates a localize.sql file in each folder. Not tested.
/// </summary>
SqlLocalizationWW,
/// <summary>
/// Other sql provider. Creates a localize.sql file in each folder.
/// </summary>
SqlLocalizationX
}
#endregion
/// <summary>
/// String holding the html
/// </summary>
protected string _fileContents;
protected const string _backupExtension = ".backup"; //this extension is used to rename the original source
protected bool _backUpSavedFiles; //whether to back up files
protected bool _changed = false; //if the file has changed
protected Dictionary<string, string> _dict; //needs to be static as MatchEvaluators are static
protected string _filePath; //assigned in Load and ConvertSiteMap to the full absolute file path.
protected ResourceStores _store; //whether global or local resx, or sql
protected string _webRoot; //the absolute path of the web root.
/// <summary>
/// Gets or sets the resource store.
/// </summary>
/// <value>The resource store.</value>
public ResourceStores ResourceStore
{
get { return _store; }
set { _store = value; }
}
/// <summary>
/// Gets or sets a value indicating whether to back up saved files.
/// Any file that is changed is first saved with a .backup extension.
/// Default is true.
/// </summary>
/// <value><c>true</c> if back up saved files; otherwise, <c>false</c>.</value>
public bool BackUpSavedFiles
{
get { return _backUpSavedFiles; }
set { _backUpSavedFiles = value; }
}
#region public ctor and run methods
/// <summary>
/// Default constructor.
/// </summary>
public Localize()
{
BackUpSavedFiles = true; //explicitly set this false if you don't want backups
ResourceStore = ResourceStores.App_LocalResources;
}
/// <summary>
/// Converts the directory. Must be called within a web application (automatically uses root).
/// Calls <see cref="ConvertSiteMap(string)"/> automatically- no need to call again
/// </summary>
public void ConvertDirectory()
{
if (HttpContext.Current == null)
throw new ApplicationException("This overload can only be used within a web application");
string path = HttpContext.Current.Server.MapPath("~/");
WebAbsoluteRoot = path;
ConvertDirectory(path);
}
/// <summary>
/// Converts the directory and all subdirectories recursively.
/// If you specified a different <see cref="WebAbsoluteRoot"/> the resources are not saved- call <see cref="SaveResources()"/> manually
/// </summary>
/// <param name="path">The path.</param>
public void ConvertDirectory(string path)
{
if (!Directory.Exists(path))
throw new ArgumentException("Directory does not exist");
CheckPrepareToConvert(path);
//change the pattern to .cs for code behind & normal classes
foreach (string f in Directory.GetFiles(path, "*.as*x")) //aspx and ascx.
{
if (f.Contains("DontTryThisOneItsGotJavaApplets")) //add others here!
continue;
ConvertFile(f);
}
foreach (string f in Directory.GetFiles(path, "*.master")) //master page(s)
{
ConvertFile(f);
}
foreach (string f in Directory.GetFiles(path, "*.sitemap")) //master page(s)
{
ConvertSiteMap(f);
}
//recurse
foreach (string sub in Directory.GetDirectories(path))
{
ConvertDirectory(sub);
}
//if we're global and we're in the root, save the resources.
if (ResourceStore == ResourceStores.App_GlobalResources && WebAbsoluteRoot.Equals(path, StringComparison.OrdinalIgnoreCase))
SaveResources(WebAbsoluteRoot, null, "AppResources");
}
private void CheckPrepareToConvert(string path)
{
//need an absolute root
if (string.IsNullOrEmpty(WebAbsoluteRoot))
{
WebAbsoluteRoot = path;
if (ResourceStore == ResourceStores.App_GlobalResources)
{
CreateNewDictionary();
LoadResources(WebAbsoluteRoot, null, null);
}
}
//rename any previous localize sql files
if (ResourceStore == ResourceStores.SqlLocalizationX || ResourceStore == ResourceStores.SqlLocalizationWW)
{
string sqlFile = Path.Combine(path, "localize.sql");
if (File.Exists(sqlFile)) File.Move(sqlFile, Path.Combine(path, "localize(old).sql"));
}
}
/// <summary>
/// Converts the file.
/// Essentially just <see cref="Load"/>, <see cref="Process"/>, <see cref="Save"/>, <see cref="SaveResources()"/>
/// </summary>
/// <param name="fullPath">The full path.
/// For the current page pass in HttpContext.Current.Server.MapPath( HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath)</param>
public void ConvertFile(string fullPath)
{
Load(fullPath);
Process();
if (ResourceDictionary.Count > 0)
{
Save(fullPath);
if (ResourceStore != ResourceStores.App_GlobalResources)
SaveResources();
}
}
/// <summary>
/// Saves the resources for a particular file. Writes <see cref="ResourceDictionary"/> to the resource file.
/// Don't use for global resources (unless it's the sitemap)
/// - use <see cref="SaveResources(string,string,string)"/> passing in null for page and class.
/// </summary>
public void SaveResources()
{
string pageName = Path.GetFileName(_filePath);
string path = Path.GetDirectoryName(_filePath);
if (pageName.EndsWith("x.cs")) pageName = pageName.Replace(".cs", ""); //if you're doing codebehind
if(pageName.EndsWith(".sitemap")) pageName = null; //sitemaps
SaveResources(path, pageName, null);
}
/// <summary>
/// Saves the resources. Use this for global resources (pass in className, and null for pageName)
/// </summary>
/// <param name="dir">The directory</param>
/// <param name="pageName">Name of the page. Pass null for global resources.</param>
/// <param name="className">Name of the class. Pass null for local resources.</param>
public void SaveResources(string dir, string pageName, string className)
{
//need an absolute root
if (string.IsNullOrEmpty(WebAbsoluteRoot)) WebAbsoluteRoot = dir;
//used for the sqlLocalization scripts
StringBuilder sb = new StringBuilder();
string sqlFile = "localize" + className + ".sql";
string s;
string resxFile;
XmlDocument doc = new XmlDocument();
switch (ResourceStore)
{
case ResourceStores.App_LocalResources:
resxFile =
Path.Combine(dir, "App_LocalResources" + Path.DirectorySeparatorChar + pageName + ".resx");
if (pageName == null)
resxFile = Path.Combine(WebAbsoluteRoot,
"App_GlobalResources" + Path.DirectorySeparatorChar + "AppResources.resx");
SaveResourceToResx(resxFile, doc);
break;
case ResourceStores.App_GlobalResources:
resxFile = Path.Combine(WebAbsoluteRoot,
"App_GlobalResources" + Path.DirectorySeparatorChar + "AppResources.resx");
SaveResourceToResx(resxFile, doc);
break;
case ResourceStores.SqlLocalizationWW:
foreach (KeyValuePair<string, string> kvp in ResourceDictionary)
{
sb.AppendLine(SaveResourceToSqlFileWW(pageName, kvp.Key, kvp.Value));
}
s = sb.ToString();
if (s != String.Empty) //only write if results
File.AppendAllText(Path.Combine(dir, sqlFile), s);
break;
case ResourceStores.SqlLocalizationX:
foreach (KeyValuePair<string, string> kvp in ResourceDictionary)
{
sb.AppendLine(SaveResourceToSqlFile(className, pageName, kvp.Key, kvp.Value));
}
s = sb.ToString();
if (s != String.Empty) //only write if results
File.AppendAllText(Path.Combine(dir, sqlFile), s);
break;
default:
break;
}
ResourceDictionary.Clear(); //in case it gets called twice
}
/// <summary>
/// Loads the resources. Only applicable to resx files. Use this overload for local resx
/// </summary>
/// <param name="fullPathToFile">The full path to file.</param>
public void LoadResources(string fullPathToFile)
{
string pageName = Path.GetFileName(fullPathToFile);
string path = Path.GetDirectoryName(fullPathToFile);
if (pageName.EndsWith("x.cs")) pageName = pageName.Replace(".cs", ""); //if you're doing codebehind
LoadResources(path, pageName, null);
}
/// <summary>
/// Loads the resources. Only applicable to resx files. Use this overload for global resx with page and class= null
/// </summary>
/// <param name="dir">The dir.</param>
/// <param name="pageName">Name of the page.</param>
/// <param name="className">Name of the class.</param>
private void LoadResources(string dir, string pageName, string className)
{
if (ResourceDictionary == null) CreateNewDictionary();
string resxFile;
switch (ResourceStore)
{
case ResourceStores.App_LocalResources:
resxFile =
Path.Combine(dir, "App_LocalResources" + Path.DirectorySeparatorChar + pageName + ".resx");
if (pageName == null)
resxFile =
Path.Combine(WebAbsoluteRoot,
"App_GlobalResources" + Path.DirectorySeparatorChar + className + ".resx");
LoadResx(resxFile);
break;
case ResourceStores.App_GlobalResources:
resxFile =
Path.Combine(WebAbsoluteRoot,
"App_GlobalResources" + Path.DirectorySeparatorChar + "AppResources.resx");
LoadResx(resxFile);
break;
}
}
private void LoadResx(string resxFile)
{
if (File.Exists(resxFile))
{
XPathDocument docNav = new XPathDocument(resxFile);
XPathNavigator nav = docNav.CreateNavigator();
XPathNodeIterator iter = nav.Select("/root/data");
while (iter.MoveNext())
{
XPathNavigator dataNav = iter.Current;
string key = dataNav.GetAttribute("name", "");
XPathNavigator valueNav = dataNav.SelectSingleNode("value");
string value = valueNav.Value;
ResourceDictionary.Add(key, value);
}
}
}
/// <summary>
/// The minimum xml to create a Resx. The actual files include an xsd but it doesn't seem to be needed.
/// </summary>
/// <returns></returns>
private static string baseResxXml()
{
return
@"<root><resheader name=""resmimetype"">
<value>text/microsoft-resx</value>
</resheader>
<resheader name=""version"">
<value>2.0</value>
</resheader>
<resheader name=""reader"">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name=""writer"">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>";
}
#region Load Public Methods
/// <summary>
/// Load an individual file.
/// </summary>
/// <param name="path"></param>
public void Load(string path)
{
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(path);
if (!File.Exists(path))
throw new ArgumentException(path);
_fileContents = File.ReadAllText(path);
_filePath = path;
}
#endregion
#region Save Public Methods
/// <summary>
/// Save output to a file path (NB: only if changed). Automatically saves original file with .orig extension.
/// </summary>
/// <param name="file"></param>
public void Save(string file)
{
if (_fileContents == null)
throw new ApplicationException("Have not loaded yet");
//don't change it if nothing different
if (_changed || string.IsNullOrEmpty(_fileContents))
{
string origfile = Path.ChangeExtension(file, _backupExtension);
//only create the origin file once. if this is run twice, leave it
if (BackUpSavedFiles && File.Exists(file) && !File.Exists(origfile))
File.Move(file, origfile);
using (StreamWriter writer = new StreamWriter(file))
{
writer.Write(_fileContents);
}
}
}
#endregion
#endregion
/// <summary>
/// Runs the conversion.
/// </summary>
public virtual void Process()
{
if (string.IsNullOrEmpty(_fileContents))
throw new ApplicationException("Must Load first");
string orig = _fileContents;
if (ResourceStore != ResourceStores.App_GlobalResources || ResourceDictionary == null)
//if global, the dictionary is held throughout
{
CreateNewDictionary();
LoadResources(WebAbsoluteRoot, null, null);
}
_changed = false;
FindTags();
FindCodeLocalizations();
VerifyKeys(); //this is primarily for debugging/ reference
if (orig != _fileContents)
_changed = true;
}
/// <summary>
/// Creates the new dictionary.
/// </summary>
private void CreateNewDictionary()
{
ResourceDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Finds a common business class that takes a localizable string. Typically this is in code behind, but may be in a code block in the page.
/// Searching for .Text=" is best done manually unfortunately (too much scope for error)
/// </summary>
private void FindCodeLocalizations()
{
//eg new Exception("text") - probably should be a regex to catch the first quote
string findstr = "BusinessValidationError(";
if (_fileContents.Contains(findstr))
{
int i = 0;
while ((i = _fileContents.IndexOf(findstr, i)) != -1)
{
i = i + findstr.Length;
//already converted (trimmed because could be split with spaces or line feeds)
if (_fileContents.Substring(i).TrimStart().StartsWith("GetLocalResourceObject"))
continue;
if (_fileContents.Substring(i).TrimStart().StartsWith("GetGlobalResourceObject"))
continue;
_fileContents = ChangeText(_fileContents, i, false);
}
}
}
/// <summary>
/// Loads each of the GetLocalResourceObject keys it finds. Useful for debugging.
/// </summary>
private void VerifyKeys()
{
if (HttpContext.Current == null)
return; //this only works within the web app (HttpContext exists)
string findstr = "GetLocalResourceObject(";
if (_fileContents.Contains(findstr))
{
int i = 0;
while ((i = _fileContents.IndexOf(findstr, i)) != -1)
{
i = i + findstr.Length;
CheckKey(_fileContents, i);
}
}
findstr = "GetGlobalResourceObject(";
if (_fileContents.Contains(findstr))
{
int i = 0;
while ((i = _fileContents.IndexOf(findstr, i)) != -1)
{
i = i + findstr.Length;
CheckKey(_fileContents, i);
}
}
}
/// <summary>
/// Write the key and value out- for debugging
/// </summary>
/// <param name="s">The string.</param>
/// <param name="i">The start position.</param>
private void CheckKey(string s, int i)
{
int startQuote = s.IndexOf("\"", i); //find the first quote
if (startQuote == -1)
return;
startQuote++; //we want after the quote
int endQuote = s.IndexOf("\"", startQuote); //find the end quote
if (endQuote == -1)
return;
int length = endQuote - startQuote;
string key = s.Substring(startQuote, length);
string pageName = _filePath.Replace(WebAbsoluteRoot, "~/"); //dirty way to make relative
if (pageName.EndsWith("x.cs")) pageName = pageName.Replace(".cs", ""); //if you're doing codebehind
//forces a get, so it should come back okay
string value = HttpContext.GetLocalResourceObject("~/" + pageName, key) as string;
Debug.WriteLine(pageName + " : " + key + " = " + value);
}
/// <summary>
/// Finds the tags using a regular expression (each is processed with <see cref="CheckTag"/>)
/// </summary>
private void FindTags()
{
//this regular expression should get attributes as well (not http-equiv as an attribute though)
//regex from http://haacked.com/archive/2005/04/22/Matching_HTML_With_Regex.aspx
//fixed with allowing for server blocks (assumes fairly well formed)
Regex regex =
new Regex(
@"</?(?<tag>[\w:]+)((?<attpair>(\s+(?<attribute>[A-Za-z0-9-]+)(?<equals>\s*=\s*(?<value>(?:(?<quote2value>"".*?"")|<%.*?%>|(?<quote1value>'.*?')|(?<nakedvalue>[^'"">\s]+))))?)|(\s*<%.*?%>))+\s*|\s*|\s*<%.*?%>\s*)/?>",
RegexOptions.Singleline);
// multiline affects ^$ only- we want \s to include \n
_fileContents = regex.Replace(_fileContents, new MatchEvaluator(CheckTag));
}
/// <summary>
/// The MatchEvaluator which takes each tag and loops through the attributes
/// </summary>
/// <param name="m">The match</param>
/// <returns></returns>
private string CheckTag(Match m)
{
string s = m.Value;
string tag = m.Groups["tag"].Value;
Debug.WriteLine("Tag " + tag);
//asp filter - only asp tags can be localized
//remove this to quickly localize non-asp then change them manually
if (!tag.Contains("asp:"))
return s; //only asp tags can be localized
string Id = FindID(m);
//second pass, after having got ID
for (int k = 0; k < m.Groups["value"].Captures.Count; k++)
{
Capture c = m.Groups["value"].Captures[k];
string atName = m.Groups["attribute"].Captures[k].Value.Trim();
string value = c.Value.Trim();
string unquoted = value.Trim(new char[] {'"', '\''});
Debug.WriteLine(atName + " = " + value);
int i = c.Index - m.Index;
if (!IsLocalizable(atName))
continue;
if (unquoted.Length < 2) //single character or empty
continue;
if (value.Contains("<%"))
//contains some server tags (<%# Bind or <% resources etc)- either done or too complex to touch
continue;
//key is Id.Attribute (eg TextBox1.Text). If two fields have the same text, they both get the same key.
string key = Id + "." + atName;
if (ResourceStore == ResourceStores.App_GlobalResources)
//if global, id.attribute doesn't make sense- use the text
key = CreateKey(unquoted);
else if (Id.Equals(string.Empty)) //TemplateFields etc don't have IDs so shorten and uppercase the value
key = CreateKey(unquoted);
if (key == String.Empty) continue;
if (value.Equals(unquoted))
//the programmer didn't quote, naughty but compilable. Not compilable with resources tag though...
{
int startQuote = s.IndexOf(unquoted, i); //the position of the first bit of unquoted
s = s.Insert(startQuote + unquoted.Length, "\"");
s = s.Insert(startQuote, "\"");
}
s = ChangeAttributeValue(s, atName, unquoted, key, true, i);
}
return s;
}
/// <summary>
/// Finds the ID for this tag.
/// </summary>
/// <param name="m">The match</param>
/// <returns></returns>
private static string FindID(Match m)
{
//find ID
string Id = String.Empty;
for (int k = 0; k < m.Groups["attribute"].Captures.Count; k++)
{
Capture c = m.Groups["attribute"].Captures[k];
string v = c.Value.Trim(); //should have been lowercased
if (v.Equals("ID", StringComparison.OrdinalIgnoreCase))
{
Capture c2 = m.Groups["value"].Captures[k];
string value = c2.Value.Trim();
Id = value.Trim(new char[] {'"', '\''});
break;
}
}
return Id;
}
private string ChangeAttributeValue(string s, string atName, string text, string key, bool isAsp, int position)
{
if (IgnoreResourceValues(text))
return s;
int startQuote = s.IndexOf(text, position); //the position of the first bit of unquoted
if (startQuote == -1) //things have got very confused
{
//if the text is "asp" you'll get the tag- so we find the attribute name then text and hope we've got it
startQuote = s.IndexOf(atName); //find the attribute
startQuote = s.IndexOf(text, startQuote); //then find the text
}
int length = text.Length;
key = SaveResourceKeyValue(text, key);
s = InsertResourceKey(s, isAsp, startQuote, length, text, key);
return s;
}
/// <summary>
/// Determines whether the specified attribute is localizable.
/// </summary>
/// <param name="attName">Name of the attribute</param>
/// <returns>
/// <c>true</c> if the specified attribute name is localizable; otherwise, <c>false</c>.
/// </returns>
private static bool IsLocalizable(string attName)
{
bool localizable = false;
if (attName.Equals("value", StringComparison.OrdinalIgnoreCase))
//normal html buttons , only if asp filter in CheckTags is removed
localizable = true;
else if (attName.Equals("Text", StringComparison.OrdinalIgnoreCase))
localizable = true;
else if (attName.Equals("HeaderText", StringComparison.OrdinalIgnoreCase))
localizable = true;
else if (attName.Equals("EmptyDataText", StringComparison.OrdinalIgnoreCase))
localizable = true;
else if (attName.Equals("ErrorMessage", StringComparison.OrdinalIgnoreCase))
localizable = true;
else if (attName.Equals("ToolTip", StringComparison.OrdinalIgnoreCase))
localizable = true;
else if (attName.Equals("SelectText", StringComparison.OrdinalIgnoreCase))
localizable = true;
//other things that may be done- CancelText etc in GridView
//wizard buttons ... StepNextButtonText="<%$ Resources: NEXT %>" StartNextButtonText="<%$ Resources: NEXT %>" StepPreviousButtonText="<%$ Resources: PREVIOUS %>" FinishPreviousButtonText="<%$ Resources: PREVIOUS %>" FinishCompleteButtonText="<%$ Resources: FINISH %>" CancelButtonText="<%$ Resources: CANCEL %>"
return localizable;
}
private string ChangeText(string s, int i, bool isAsp)
{
int startQuote = s.IndexOf("\"", i); //find the first quote
if (startQuote == -1)
startQuote = s.IndexOf("'", i); //single quotes
if (startQuote == -1)
return s;
startQuote++; //we want after the quote
int endQuote = s.IndexOf("\"", startQuote); //find the end quote
if (endQuote == -1)
endQuote = s.IndexOf("'", startQuote); //single quotes
if (endQuote == -1)
return s;
int length = endQuote - startQuote;
string text = s.Substring(startQuote, length);
if (IgnoreResourceValues(text))
return s;
//
string key = CreateKey(text);
if (key == "")
return s; //only punctuation in the text - don't localize
key = SaveResourceKeyValue(text, key);
s = InsertResourceKey(s, isAsp, startQuote, length, text, key);
return s;
}
/// <summary>
/// Generates a resource key from the text (uppercased alphnumeric).
/// </summary>
/// <param name="text">The text.</param>
/// <returns></returns>
private static string CreateKey(string text)
{
string key = Regex.Replace(text, "[^A-Za-z0-9]", "");
key = key.ToUpper(); //because the database has a case insensitive unique constraint
if (key.Length > 20) key = key.Substring(0, 20); //for very long text
return key;
}
private string InsertResourceKey(string s, bool isAsp, int startQuote, int length, string text, string key)
{
string prefix;
string suffix;
if (isAsp)
{
//no quotes
prefix = "<%$ Resources: ";
suffix = " %>";
//preserve surrounding quotes
}
else
{
//key must be quoted
prefix = "GetLocalResourceObject(\"";
if (ResourceStore == ResourceStores.App_GlobalResources)
prefix = "GetGlobalResourceObject(\"";
suffix = "\") as string";
//kill surrounding quotes
startQuote = startQuote - 1;
length = length + 2;
}
string resrc = prefix +
(ResourceStore == ResourceStores.App_GlobalResources ? "AppResources," : "")
+ key + suffix;
s = s.Remove(startQuote, length);
s = s.Insert(startQuote, resrc);
Debug.WriteLine(
s.Substring((startQuote > 20 ? startQuote - 20 : 0),
(s.Length - startQuote > 80 ? 80 : s.Length - startQuote)));
Debug.WriteLine(s + " [" + text + "]");
return s;
}
/// <summary>
/// Ignore certain resource values because they are not really localizable. Single letters (A, B...) or VCR buttons
/// </summary>
/// <param name="text">The text.</param>
/// <returns>
/// <c>true</c> if should be ignored; otherwise, <c>false</c>.
/// </returns>
private static bool IgnoreResourceValues(string text)
{
text = text.Trim();
if (text.Length < 2)
return true; //a single letter or number- don't localize
//"VCR" buttons- ignore these
else if (text.Equals("<<", StringComparison.OrdinalIgnoreCase))
return true; //<<
else if (text.Equals("<", StringComparison.OrdinalIgnoreCase))
return true; //<
else if (text.Equals(">>", StringComparison.OrdinalIgnoreCase))
return true; //>>
else if (text.Equals(">", StringComparison.OrdinalIgnoreCase))
return true; //>
return false;
}
/// <summary>
/// Saves the resource key and value.
/// <para>If the value already exists elsewhere on the page, re-use that key.</para>
/// <para>If the key is already used, we modify it to create a unqiue key.</para>
/// </summary>
/// <param name="text">The text.</param>
/// <param name="key">The key.</param>
/// <returns>tThe key (may be different - see summary)</returns>
private string SaveResourceKeyValue(string text, string key)
{
if (ResourceDictionary.ContainsValue(text))
{
//find key
foreach (KeyValuePair<string, string> kvp in ResourceDictionary)
{
if (kvp.Value.Equals(text)) key = kvp.Key;
}
}
else
{
while (ResourceDictionary.ContainsKey(key))
{
key += "1"; //unless the key was because the text is the same- we check next
}
ResourceDictionary.Add(key, text);
}
return key;
}
#region Helpers
/* NOT USED
/// <summary>
/// Utility to manually change a tag to another one, given just the start of the tag.
/// </summary>
/// <param name="s">the full string</param>
/// <param name="find">the substring to find (the start of the tag)</param>
/// <param name="replace">the substring to replace</param>
/// <returns></returns>
private string ReplaceTagStarting(string s, string find, string replace)
{
int start = s.IndexOf(find, StringComparison.CurrentCultureIgnoreCase);
if (start == -1)
return s;
int end = FindEndOfTag(s, start);
Debug.WriteLine("Replace [" + start + " to " + end + "] =" +
s.Substring(start + 1, end - start) +
" \nreplaced by " + replace);
s = s.Substring(0, start) + replace + s.Substring(end + 1);
return s;
}
private int FindEndOfTag(string htmlFragment, int start)
{
//assuming we're at the start of the tag just find the next >
int end = htmlFragment.IndexOf('>', start);
//if there are nested asp tags, ignore 'em
if (end != -1 && htmlFragment.Substring(end - 1, 2) == "%>")
//add one to skip to next endbrace
end = FindEndOfTag(htmlFragment, end + 1);
if (end != -1 && htmlFragment.Substring(end - 1, 2) == "<>")
//add one to skip to next endbrace
end = FindEndOfTag(htmlFragment, end + 1);
return end;
}
*/
#endregion
#region Properties
/// <summary>
/// Gets or sets the resource dictionary. It is refreshed with each file (except when using App_GlobalResources).
/// It is recreated in <see cref="Process"/> and written to the store in <see cref="SaveResources()"/>
/// </summary>
/// <value>The resource dictionary.</value>
public Dictionary<string, string> ResourceDictionary
{
get { return _dict; }
set { _dict = value; }
}
/// <summary>
/// Gets or sets the web absolute root. If not explicitly set, we assume it is the first directory it finds.
/// </summary>
/// <value>The web absolute root.</value>
public string WebAbsoluteRoot
{
get { return _webRoot; }
set { _webRoot = value; }
}
#endregion
#region SiteMap fixing
/// <summary>
/// Converts the site map. Only use this overload in a webapplication. If you use <see cref="ConvertDirectory()"/> this is done for you.
/// </summary>
public void ConvertSiteMap()
{
if (HttpContext.Current == null)
throw new ApplicationException("This overload can only be used within a web application");
string path = HttpContext.Current.Server.MapPath("~/");
ConvertSiteMap(path);
}
/// <summary>
/// Converts the site map. If you use <see cref="ConvertDirectory()"/> this is done for you.
/// <para>If global DOES NOT SAVE RESOURCE- use <see cref="SaveResources()"/></para>
/// <code>SaveResources(path, "", "AppResources");</code>
/// </summary>
/// <param name="path">The path.</param>
public void ConvertSiteMap(string path)
{
XmlDocument doc = new XmlDocument();
//if they just passed in the directory, assume they mean web.sitemap
if (!path.EndsWith(".sitemap", StringComparison.OrdinalIgnoreCase))
path = Path.Combine(path, "Web.sitemap");
if (!File.Exists(path))
return;
_filePath = path;
doc.Load(path);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("ns", "http://schemas.microsoft.com/AspNet/SiteMap-File-1.0");
if (ResourceDictionary == null || ResourceStore != ResourceStores.App_GlobalResources)
//if global, the dictionary is held throughout
CreateNewDictionary();
bool changed = false;
foreach (XmlElement siteMapNode in doc.SelectNodes("//ns:siteMapNode", nsmgr))
{
//we presume this is unique
string key = siteMapNode.GetAttribute("url");
if (MakeSitemapResource("title", siteMapNode, key)) changed = true;
if (MakeSitemapResource("description", siteMapNode, key + ".description")) changed = true;
}
if (changed)
{
if (ResourceStore != ResourceStores.App_GlobalResources)
SaveResources(Path.GetDirectoryName(path), null, "sitemap");
doc.DocumentElement.SetAttribute("enableLocalization", "true");
doc.Save(path);
}
}
private bool MakeSitemapResource(string attribute, XmlElement siteMapNode, string key)
{
string value = siteMapNode.GetAttribute(attribute);
if (value == "")
return false;
if (value.ToLower().Contains("$resources:")) //string.contains doesn't have a stringcomparison overload :(
return false; //already done
//we could check if value is already there and reuse the key...
if (key == "")
key = value;
//get rid of confusing characters
key = key.Replace(" ", "")
.Replace(".aspx", "")
.Replace(".", "")
.Replace("#", "")
.Replace("=", "")
.Replace("&", "")
.Replace("/", "")
.Replace("?", "")
.Replace("'", "")
.Replace("_", "")
.Replace("-", "");
if (key.Equals("default", StringComparison.OrdinalIgnoreCase))
key = "_default"; //The resource name 'default' is not a valid identifier
//check dictionary
while (ResourceDictionary.ContainsKey(key))
{
key += "1"; //simplest way of doing it, sorry
}
ResourceDictionary.Add(key, value);
//always global
siteMapNode.SetAttribute(attribute, "$resources:AppResources," + key);
return true;
}
#endregion
#region Save resources to resx or sql files
private void SaveResourceToResx(string resxFile, XmlDocument doc)
{
if (!File.Exists(resxFile))
{
if (!Directory.Exists(Path.GetDirectoryName(resxFile)))
Directory.CreateDirectory(Path.GetDirectoryName(resxFile));
File.WriteAllText(resxFile, baseResxXml());
}
doc.Load(resxFile);
foreach (KeyValuePair<string, string> kvp in ResourceDictionary)
{
if (doc.SelectSingleNode("/root/data[@name='" + kvp.Key + "']") != null)
continue; //already exists
XmlElement data = doc.CreateElement("data");
data.SetAttribute("name", kvp.Key);
XmlElement value = doc.CreateElement("value");
value.InnerText = kvp.Value;
data.AppendChild(value);
doc.DocumentElement.AppendChild(data);
}
doc.Save(resxFile);
}
private static string SaveResourceToSqlFile(string globalResourceClass, string localPageName, string key,
string value)
{
const string defaultCulture = "'en'"; //with single quotes
//very bad, but..
value = value.Replace("'", "''");
//basic sanitize. If you have sql injection code in your text, don't blame me
if (string.IsNullOrEmpty(globalResourceClass))
globalResourceClass = "null";
else
globalResourceClass = "'" + globalResourceClass + "'";
if (string.IsNullOrEmpty(localPageName))
localPageName = "null";
else
localPageName = "'" + localPageName + "'";
string s = "INSERT INTO [LOCALIZATION] " +
"([VIRTUAL_PATH],[CLASS_NAME],[CULTURE_NAME],[RESOURCE_NAME],[RESOURCE_VALUE])";
s += " VALUES (" + localPageName + ", " + globalResourceClass + "," + defaultCulture + ",'" + key + "','" +
value + "');";
return s;
}
private static string SaveResourceToSqlFileWW(string resourceSet, string key, string value)
{
//very bad, but..
value = value.Replace("'", "''");
//basic sanitize. If you have sql injection code in your text, don't blame me
if (string.IsNullOrEmpty(resourceSet))
resourceSet = "null";
else
resourceSet = "'" + resourceSet + "'";
//below is the schema for the Rick Strahl localization table
string s = "INSERT INTO [LOCALIZATION] " +
"([RESOURCESET],[RESOURCEID],[VALUE])";
s += " VALUES (" + resourceSet + ", '" + key + "','" + value + "');";
return s;
}
#endregion
}
}