I wrote a small console application (source below) to find and possibly rename files containing international characters, as they are a source of constant pain in most version control systems (some of which are given below). The code I use has a simple dictionary with characters to look for and replace (and damage every other character that uses more than one byte of memory), but it feels very hacky. What is the correct way (a) to find out if a symbol is international? and (b) what would be the best ASCII replacement character?
Let me provide some background information on why this is necessary. It so happened that the Danish character Å has two different encodings in UTF-8, both representing the same character. They are known as NFC and NFD encodings. Windows and Linux will create the default NFC encoding, but respect any encoding that it sets. A Mac converts all names (when saved to an HFS + partition) to NFD and therefore returns a different stream of bytes for the file name created in Windows. This effectively destroys Subversion, Git, and many other utilities that do not want to process this script correctly.
I am currently evaluating Mercurial, which is even worse when handling international characters. Being tired enough of these problems, you will either need to control the source code, or international in nature, and therefore we are here.
My current implementation:
public class Checker
{
private Dictionary<char, string> internationals = new Dictionary<char, string>();
private List<char> keep = new List<char>();
private List<char> seen = new List<char>();
public Checker()
{
internationals.Add( 'æ', "ae" );
internationals.Add( 'ø', "oe" );
internationals.Add( 'å', "aa" );
internationals.Add( 'Æ', "Ae" );
internationals.Add( 'Ø', "Oe" );
internationals.Add( 'Å', "Aa" );
internationals.Add( 'ö', "o" );
internationals.Add( 'ü', "u" );
internationals.Add( 'ä', "a" );
internationals.Add( 'é', "e" );
internationals.Add( 'è', "e" );
internationals.Add( 'ê', "e" );
internationals.Add( '¦', "" );
internationals.Add( 'Ã', "" );
internationals.Add( '©', "" );
internationals.Add( ' ', "" );
internationals.Add( '§', "" );
internationals.Add( '¡', "" );
internationals.Add( '³', "" );
internationals.Add( '', "" );
internationals.Add( 'º', "" );
internationals.Add( '«', "-" );
internationals.Add( '»', "-" );
internationals.Add( '´', "'" );
internationals.Add( '`', "'" );
internationals.Add( '"', "'" );
internationals.Add( Encoding.UTF8.GetString( new byte[] { 226, 128, 147 } )[ 0 ], "-" );
internationals.Add( Encoding.UTF8.GetString( new byte[] { 226, 128, 148 } )[ 0 ], "-" );
internationals.Add( Encoding.UTF8.GetString( new byte[] { 226, 128, 153 } )[ 0 ], "'" );
internationals.Add( Encoding.UTF8.GetString( new byte[] { 226, 128, 166 } )[ 0 ], "." );
keep.Add( '-' );
keep.Add( '=' );
keep.Add( '\'' );
keep.Add( '.' );
}
public bool IsInternationalCharacter( char c )
{
var s = c.ToString();
byte[] bytes = Encoding.UTF8.GetBytes( s );
if( bytes.Length > 1 && ! internationals.ContainsKey( c ) && ! seen.Contains( c ) )
{
Console.WriteLine( "X '{0}' ({1})", c, string.Join( ",", bytes ) );
seen.Add( c );
if( ! keep.Contains( c ) )
{
internationals[ c ] = "";
}
}
return internationals.ContainsKey( c );
}
public bool HasInternationalCharactersInName( string name, out string safeName )
{
StringBuilder sb = new StringBuilder();
Array.ForEach( name.ToCharArray(), c => sb.Append( IsInternationalCharacter( c ) ? internationals[ c ] : c.ToString() ) );
int length = sb.Length;
sb.Replace( " ", " " );
while( sb.Length != length )
{
sb.Replace( " ", " " );
}
safeName = sb.ToString().Trim();
string namePart = Path.GetFileNameWithoutExtension( safeName );
if( namePart.EndsWith( "." ) )
safeName = namePart.Substring( 0, namePart.Length - 1 ) + Path.GetExtension( safeName );
return name != safeName;
}
}
And it will be called as follows:
FileInfo file = new File( "Århus.txt" );
string safeName;
if( checker.HasInternationalCharactersInName( file.Name, out safeName ) )
{
}