Skip to content

Commit

Permalink
Language file scanning tool for EPiServer websites.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mathias Kunto committed Feb 26, 2014
1 parent aed8084 commit 0d7293b
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 0 deletions.
6 changes: 6 additions & 0 deletions EPiServerLanguageScanner/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>
64 changes: 64 additions & 0 deletions EPiServerLanguageScanner/LanguageScanner.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{1D6AE8D4-469C-4D9E-A1C6-DF9B38959C2F}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LanguageScanner</RootNamespace>
<AssemblyName>LanguageScanner</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="NDesk.Options">
<HintPath>packages\NDesk.Options.0.2.1\lib\NDesk.Options.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Translation.cs" />
<Compile Include="XElementExtensions.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
20 changes: 20 additions & 0 deletions EPiServerLanguageScanner/LanguageScanner.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageScanner", "LanguageScanner.csproj", "{1D6AE8D4-469C-4D9E-A1C6-DF9B38959C2F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1D6AE8D4-469C-4D9E-A1C6-DF9B38959C2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D6AE8D4-469C-4D9E-A1C6-DF9B38959C2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D6AE8D4-469C-4D9E-A1C6-DF9B38959C2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D6AE8D4-469C-4D9E-A1C6-DF9B38959C2F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
138 changes: 138 additions & 0 deletions EPiServerLanguageScanner/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using NDesk.Options;

namespace LanguageScanner
{
class Program
{
private static readonly Stopwatch StopWatch = new Stopwatch();
private static string[] _excludeExtensions =
{
"dll", "xml", "pdb", "csproj", "designer.cs", "config", "cache", "swf", "css",
"js", "jpg", "gif", "png", "bmp", "psd", "html", "orig", "txt", "fla", "pdf"
};
private static bool _ignoreCase = false;
private static bool _distinctTranslations = false;

static void Main(string[] args)
{
var rootPath = Directory.GetCurrentDirectory();
var langPath = string.Concat(Directory.GetCurrentDirectory(), @"\lang");
var printHelpAndExit = false;

var options = new OptionSet
{
{ "rp|rootPath=", string.Format("Path to the directory containing your source code; default is '{0}'", rootPath), arg => rootPath = arg },
{ "lp|langPath=", string.Format("Path to the directory containing your language files; default is '{0}'", langPath), arg => langPath = arg },
{ "ee|excludeExtensions=", string.Format("File extensions to exclude in code scan; default are {0}", string.Join(",", _excludeExtensions)), arg => _excludeExtensions = arg == null ? _excludeExtensions : arg.Split(',') },
{ "ic|ignoreCase", string.Format("Makes the scan case insensitive, increases working time; default is '{0}'", _ignoreCase.ToString().ToLower()), arg => _ignoreCase = arg != null },
{ "dt|distinctTranslations", string.Format("Remove duplicate XPaths, scan faster but file list will be inaccurate; default is '{0}'", _distinctTranslations), arg => _distinctTranslations = arg != null },
{ "h|?|help", "Shows the help", arg => printHelpAndExit = arg != null },
};
try
{
var unknown = options.Parse(args);
if (unknown.Any() || printHelpAndExit)
{
options.WriteOptionDescriptions(Console.Out);
Environment.Exit(0);
}
StopWatch.Start();

Console.Out.WriteLine("--- Starting to scan language files for translations ---");
var translations = ScanLanguageFilesIn(langPath).ToList();
var langPathTime = StopWatch.Elapsed;
Console.Out.WriteLine("Language files scanned in: {0}", langPathTime.ToString("g"));
Console.Out.WriteLine("Number of translations: {0}", translations.Count());

Console.Out.WriteLine("--- Starting to retrieve file paths for code files ---");
var filePaths = FilePathsIn(rootPath).ToArray();
var getFilePathsTime = StopWatch.Elapsed;
Console.Out.WriteLine("File paths retrieved in: {0}", getFilePathsTime.ToString("g"));
Console.Out.WriteLine("Number of file paths: {0}", filePaths.Count());

Console.Out.WriteLine("--- Starting to scan for orphans ---");

var possibleOrphans = translations.ConvertAll(t=>t);
foreach (var path in filePaths)
{
Console.Out.WriteLine("Starting to scan file: {0}", path);
var content = _ignoreCase ? File.ReadAllText(path).ToLower() : File.ReadAllText(path);

var usedTranslations = translations.Where(translation => content.Contains(translation.XPath));
foreach (var translation in usedTranslations)
{
possibleOrphans.Remove(translation);
}

Console.Out.WriteLine("Done! Possible orphas now: {0}", possibleOrphans.Count());
}

var scanCompleteTime = StopWatch.Elapsed;
StopWatch.Stop();

foreach (var orphan in possibleOrphans)
{
Console.Out.WriteLine("{0}: {1} ({2})", orphan.File, orphan.XPath, orphan.Content);
}
Console.Out.WriteLine("**************************************");
Console.Out.WriteLine("Language files scanned in: {0}", langPathTime.ToString("g"));
Console.Out.WriteLine("File paths retrieved in: {0}", getFilePathsTime.ToString("g"));
Console.Out.WriteLine("Scan compleated in: {0}", scanCompleteTime.ToString("g"));
Console.Out.WriteLine("Possible orphans found: {0}", possibleOrphans.Count);
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
}
}

private static IEnumerable<string> FilePathsIn(string currentDirectory)
{
var paths = Directory.GetFiles(currentDirectory)
.Where(file => !ShouldExclude(file))
.ToList();
foreach (var subDirectory in Directory.GetDirectories(currentDirectory))
{
paths.AddRange(FilePathsIn(subDirectory));
}
return paths;
}

private static bool ShouldExclude(string file)
{
return _excludeExtensions.Any(extension => file.ToLower().EndsWith(extension.ToLower()));
}

private static IEnumerable<Translation> ScanLanguageFilesIn(string langPath)
{
var translations = new List<Translation>();
var paths = Directory.GetFiles(langPath)
.Where(file => file.EndsWith(".xml"))
.ToArray();
foreach (var file in paths)
{
translations.AddRange(TranslationsIn(file));
}
return _distinctTranslations ? translations.Distinct() : translations;
}

private static IEnumerable<Translation> TranslationsIn(string file)
{
var doc = XDocument.Load(file);
var leaves = from e in doc.Descendants() where !e.Elements().Any() select e;
return leaves
.Select(leaf => new Translation
{
File = file,
XPath = _ignoreCase ? leaf.XPath().ToLower() : leaf.XPath(),
Content = leaf.Value
});
}
}
}
35 changes: 35 additions & 0 deletions EPiServerLanguageScanner/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("EPiServer Language Scanner")]
[assembly: AssemblyDescription("Scans language files for translations and then looks through source code for usages of these")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LanguageScanner")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("02472c42-7ca5-4faa-acd1-67976c12ddd4")]

// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
20 changes: 20 additions & 0 deletions EPiServerLanguageScanner/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Language File Editor version 1.0 for EPiServer CMS 6 R2
http://blog.mathiaskunto.com/rss

Installation instructions:

* Drop the files in your solution.
* Update namespaces where necessary.
* Update JavaScript and CSS paths in LanguageFileEditor.aspx if necessary.
* Update Plugin path in LanguageFileEditor.aspx.cs if necessary.
* Update LoadControl path in EditableXmlNode.ascx.cs if necessary.
* Reference Newtonsoft.Json.Net35.dll or later (Available at http://json.codeplex.com/releases/view/64935)

More information at:
http://blog.mathiaskunto.com/2011/09/04/allowing-web-administrators-to-dynamically-update-episerver-language-files

Well formulated bug reports and constructive feedback is always welcome.

Disclaimer:
Always make backups. You are using this at your own risk,
and cannot hold the author responsible for anything that this may cause.
16 changes: 16 additions & 0 deletions EPiServerLanguageScanner/Translation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace LanguageScanner
{
public class Translation : IComparable<Translation>
{
public string File { get; set; }
public string XPath { get; set; }
public string Content { get; set; }

public int CompareTo(Translation other)
{
return String.Compare(this.XPath, other.XPath, StringComparison.Ordinal);
}
}
}
22 changes: 22 additions & 0 deletions EPiServerLanguageScanner/XElementExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Xml.Linq;

namespace LanguageScanner
{
public static class XElementExtensions
{
public static string XPath(this XElement element)
{
var tmp = element;
var path = string.Empty;
while (tmp != null)
{
path = string.Concat("/", tmp.Name, path);
tmp = tmp.Parent;
}

// EPiServer's language files starts with <languages><language>, and we don't want those in our paths.
var startIndex = "/languages/language".Length;
return path.Substring(startIndex, path.Length - startIndex);
}
}
}
4 changes: 4 additions & 0 deletions EPiServerLanguageScanner/packages.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NDesk.Options" version="0.2.1" targetFramework="net45" />
</packages>
7 changes: 7 additions & 0 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ Adds the possibility to create links to EPiServer's bookmarks in TinyMCE.



EPiServerLanguageScanner
http://blog.mathiaskunto.com/

Tool that looks through EPiServer style language files scanning your source code for the ones that are orphaned; i.e. are not used in your code.



ExampleMVPVisualStudio2010Solution
http://blog.mathiaskunto.com/2012/02/27/complete-and-concrete-example-of-what-an-asp-net-webforms-model-view-presenter-project-may-look-like-using-structuremap-nunit-and-automoq/

Expand Down

0 comments on commit 0d7293b

Please sign in to comment.