using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Globalization; using System.Web.Script.Serialization; using System.Collections.Specialized; using System.Collections; using System.Text.RegularExpressions; using System.Reflection; using System.Diagnostics; using Globalization; namespace Globalization { public class GlobalizationInfo { public string name = ""; public string englishName; public string nativeName; public string language; public bool isRTL; public NumberFormatInfo numberFormat; public Dictionary calendars; private CultureInfo culture; public static Dictionary BasisGlobInfo; private static string[] _numberNegativePatterns = "(n)|-n|- n|n-|n -".Split('|'); private static string[] _currencyNegativePatterns = "($n)|-$n|$-n|$n-|(n$)|-n$|n-$|n$-|-n $|-$ n|n $-|$ n-|$ -n|n- $|($ n)|(n $)".Split('|'); private static string[] _percentNegativePatterns = "-n %|-n%|-%n|%-n|%n-|n-%|n%-|-% n|n %-|% n-|% -n|n- %".Split('|'); private static string[] _currencyPositivePatterns = "$n|n$|$ n|n $".Split('|'); private static string[] _percentPositivePatterns = "n %|n%|%n|% n".Split('|'); public static GlobalizationInfo GetGlobInfo(CultureInfo culture) { var info = new GlobalizationInfo { culture = culture, language = (culture == CultureInfo.InvariantCulture || culture.IsNeutralCulture) ? culture.Name : culture.Parent.Name, name = String.IsNullOrEmpty(culture.Name) ? "invariant" : culture.Name, englishName = String.IsNullOrEmpty(culture.Name) ? "invariant" : culture.EnglishName, nativeName = String.IsNullOrEmpty(culture.Name) ? "invariant" : culture.NativeName, isRTL = culture.TextInfo.IsRightToLeft, numberFormat = GetNumberFormatInfo(culture), calendars = GetCalendars(culture) }; return info; } public static Dictionary GetCalendars(CultureInfo culture) { var calendars = new Dictionary(); bool foundStandard = false; var gregorianType = typeof(GregorianCalendar); var defaultCalendar = culture.DateTimeFormat.Calendar; var defaultCalendarType = defaultCalendar.GetType(); GregorianCalendarTypes? gregorianCalendarType = null; if (defaultCalendarType == gregorianType) { gregorianCalendarType = ((GregorianCalendar)defaultCalendar).CalendarType; } var optionalCalendars = culture.OptionalCalendars; foreach (var calendar in optionalCalendars) { var type = calendar.GetType(); string name; bool isStandard = false; if (type == gregorianType) { var calendarType = ((GregorianCalendar)calendar).CalendarType; if (calendarType == GregorianCalendarTypes.USEnglish) { // we include the Gregorian_USEnglish culture as part of the built-in 'en' culture // because it is so common -- it is an optional calendar of every single english // speaking culture. So, skip it when found for any other culture. continue; } else if (culture == CultureInfo.InvariantCulture) { // invariant has one calendar, Gregorian_Localized, which is identical // to Gregorian_USEnglish. name = " Gregorian_USEnglish"; } else { name = "Gregorian_" + calendarType.ToString(); } if (!foundStandard && gregorianCalendarType.HasValue && gregorianCalendarType == gregorianCalendarType.Value) { isStandard = true; foundStandard = true; } } else { name = type.Name.Replace("Calendar", ""); if (!foundStandard) { isStandard = true; foundStandard = true; } } string key = name; if (isStandard) { key = "standard"; } if (culture != CultureInfo.InvariantCulture) { culture.DateTimeFormat.Calendar = calendar; } var calendarInfo = GetDateTimeFormatInfo(culture, name); calendars.Add(key, calendarInfo); } if (!foundStandard) { throw new ApplicationException("Could not locate the standard calendar type for culture '" + culture.Name + "'."); } return calendars; } public static GlobalizationInfo.NumberFormatInfo GetNumberFormatInfo(CultureInfo culture) { var nf = culture.NumberFormat; return new GlobalizationInfo.NumberFormatInfo { decimals = nf.NumberDecimalDigits, decimalSeparator = nf.NumberDecimalSeparator, groupSeparator = nf.NumberGroupSeparator, groupSizes = nf.NumberGroupSizes, NaN = nf.NaNSymbol, negative = nf.NegativeSign, negativeInfinity = nf.NegativeInfinitySymbol, positive = nf.PositiveSign, positiveInfinity = nf.PositiveInfinitySymbol, pattern = new string[] { GetFromStringList(_numberNegativePatterns, nf.NumberNegativePattern) }, currency = new GlobalizationInfo.NumberFormatInfo.NumberClassFormatInfo { decimals = nf.CurrencyDecimalDigits, decimalSeparator = nf.CurrencyDecimalSeparator, groupSeparator = nf.CurrencyGroupSeparator, groupSizes = nf.CurrencyGroupSizes, pattern = new string[] { GetFromStringList(_currencyNegativePatterns, nf.CurrencyNegativePattern), GetFromStringList(_currencyPositivePatterns, nf.CurrencyPositivePattern) }, symbol = nf.CurrencySymbol }, percent = new GlobalizationInfo.NumberFormatInfo.NumberClassFormatInfo { decimals = nf.PercentDecimalDigits, decimalSeparator = nf.PercentDecimalSeparator, groupSeparator = nf.PercentGroupSeparator, groupSizes = nf.PercentGroupSizes, pattern = new string[] { GetFromStringList(_percentNegativePatterns, nf.PercentNegativePattern), GetFromStringList(_percentPositivePatterns, nf.PercentPositivePattern) }, symbol = nf.PercentSymbol } }; } public static string[] GetAMPMDesignators(CultureInfo culture, string ampm) { return String.IsNullOrEmpty(ampm) ? null : new string[] { ampm, culture.TextInfo.ToLower(ampm), culture.TextInfo.ToUpper(ampm) }; } public static GlobalizationInfo.DateFormatInfo GetDateTimeFormatInfo(CultureInfo culture, string calendarName) { var df = culture.DateTimeFormat; var info = new GlobalizationInfo.DateFormatInfo { name = calendarName, months = new DateFormatInfo.MonthInfo { names = df.MonthNames, namesAbbr = df.AbbreviatedMonthNames }, monthsGenitive = new DateFormatInfo.MonthInfo { names = df.MonthGenitiveNames, namesAbbr = df.AbbreviatedMonthGenitiveNames }, firstDay = (int) df.FirstDayOfWeek, dateSeparator = df.DateSeparator, timeSeparator = df.TimeSeparator, days = new DateFormatInfo.DayInfo { names = df.DayNames, namesAbbr = df.AbbreviatedDayNames, namesShort = df.ShortestDayNames }, eras = GetEraInfo(culture), twoDigitYearMax = df.Calendar.TwoDigitYearMax, patterns = GetPatterns(df) }; info.AM = GetAMPMDesignators(culture, df.AMDesignator); info.PM = GetAMPMDesignators(culture, df.PMDesignator); if (df.Calendar != null) { var type = df.Calendar.GetType(); if (type == typeof(HijriCalendar)) { string convert; using (var sr = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Generator.HijriCalendar.js"))) { convert = sr.ReadToEnd(); } int adjustment = ((HijriCalendar)df.Calendar).HijriAdjustment; convert = convert.Replace("%HIJRIADJUSTMENT%", adjustment.ToString(CultureInfo.InvariantCulture)); info.convertScriptBlock = convert; } else if (type == typeof(UmAlQuraCalendar)) { using (var sr = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Generator.UmAlQuraCalendar.js"))) { info.convertScriptBlock = sr.ReadToEnd(); } } } return info; } private static GlobalizationInfo.DateFormatInfo.EraInfo[] GetEraInfo(CultureInfo culture) { Calendar cal = culture.DateTimeFormat.Calendar; List eras = null; if (cal != null) { eras = new List(); foreach (var eraNum in cal.Eras) { eras.Add(GetEraInfo(culture, eraNum)); } } return eras == null ? null : eras.ToArray(); } private static GlobalizationInfo.DateFormatInfo.EraInfo GetEraInfo(CultureInfo culture, int eraNum) { var era = new GlobalizationInfo.DateFormatInfo.EraInfo { name = culture.DateTimeFormat.GetEraName(eraNum), offset = 0, start = null }; var calendar = culture.DateTimeFormat.Calendar; Type type = calendar.GetType(); if (type != typeof(GregorianCalendar)) { if (type == typeof(TaiwanCalendar)) { era.offset = 0x777; } else if (type == typeof(KoreanCalendar)) { era.offset = -2333; } else if (type == typeof(ThaiBuddhistCalendar)) { era.offset = -543; } else if (type == typeof(JapaneseCalendar)) { switch (eraNum) { case 1: era.start = 0xdf9984200L; era.offset = 0x7c4; break; case 2: era.start = -1357603200000L; era.offset = 0x785; break; case 3: era.start = -1812153600000L; era.offset = 0x777; break; case 4: era.start = null; era.offset = 0x74b; break; default: throw new InvalidOperationException("Invalid era number for JapaneseCalendar: " + eraNum.ToString()); } } } return era; } private static Dictionary GetPatterns(DateTimeFormatInfo df) { var patterns = new Dictionary { { "d", df.ShortDatePattern }, { "D", df.LongDatePattern }, { "t", df.ShortTimePattern }, { "T", df.LongTimePattern }, { "f", df.LongDatePattern + " " + df.ShortTimePattern }, { "F", df.FullDateTimePattern }, { "M", df.MonthDayPattern }, { "S", df.SortableDateTimePattern }, { "Y", df.YearMonthPattern } }; return patterns; } private static bool ArrayEquality(Array arr1, Array arr2) { if (arr1.Length == arr2.Length) { for (int i = 0; i < arr1.Length; i++) { if (!arr1.GetValue(i).Equals(arr2.GetValue(i))) { return false; } } return true; } return false; } private static bool ListEquality(IList arr1, IList arr2) { if (arr1.Count == arr2.Count) { for (int i = 0; i < arr1.Count; i++) { var val1 = arr1[i]; var val2 = arr2[i]; if (val1 == null || !val1.Equals(val2)) { if (val1 is IList && val2 is IList) { if (!ListEquality(val1 as IList, val2 as IList)) { return false; } } else if (val1 is Dictionary && val2 is Dictionary) { var diff = DiffGlobInfos((Dictionary)val1, (Dictionary)val2); if (diff.Count > 0) { return false; } } else { return false; } } } return true; } return false; } private static string GetFromStringList(string[] list, int value) { return value < list.Length ? list[value] : null; } public Dictionary ToDictionary(bool diffCalendars) { var jss = new JavaScriptSerializer(); var str = jss.Serialize(this); var dictionary = jss.Deserialize>(str); var cals = (Dictionary) dictionary["calendars"]; Dictionary basisStandardCal = null; if (GlobalizationInfo.BasisGlobInfo != null) { basisStandardCal = (Dictionary)((Dictionary)GlobalizationInfo.BasisGlobInfo["calendars"])["standard"]; foreach (var pair in this.calendars) { var cal = (Dictionary)cals[pair.Key]; if (diffCalendars) { // make each calendar a diff from the standard basis calendar cals[pair.Key] = cal = DiffGlobInfos(basisStandardCal, cal); } // apply convert script if it exists if (!String.IsNullOrEmpty(pair.Value.convertScriptBlock)) { cal["convert"] = pair.Value.convertScriptBlock; } // remove redundant monthsGenitive array if it is equivilent to months Dictionary months = cal.ContainsKey("months") ? (Dictionary)cal["months"] : null; Dictionary monthsGenitive = cal.ContainsKey("monthsGenitive") ? (Dictionary)cal["monthsGenitive"] : null; Dictionary diff = (months != null && monthsGenitive != null) ? DiffGlobInfos(months, monthsGenitive) : null; if (diff != null && diff.Count == 0) { // the genitive months are the same as months, so remove it since it's optional cal.Remove("monthsGenitive"); } } } return dictionary; } public static string GenerateJavaScript(string extend, string global, CultureInfo culture, string name, Dictionary dictionary, StringBuilder aggregateScript) { string cultureFragment = ToJavaScript(extend, culture, dictionary, 1, false); if (aggregateScript != null) { aggregateScript.AppendFormat(CultureInfo.InvariantCulture, @" Globalize.addCultureInfo( ""{0}"", ""default"", {{ {1} }}); ", name, cultureFragment, extend); } return string.Format(CultureInfo.InvariantCulture, @"/* * Globalize Culture {0} * * http://github.com/jquery/globalize * * Copyright Software Freedom Conservancy, Inc. * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * This file was generated by the Globalize Culture Generator * Translation: bugs found in this file need to be fixed in the generator */ (function( window, undefined ) {{ var Globalize; if ( typeof require !== ""undefined"" && typeof exports !== ""undefined"" && typeof module !== ""undefined"" ) {{ // Assume CommonJS Globalize = require( ""globalize"" ); }} else {{ // Global variable Globalize = window.Globalize; }} Globalize.addCultureInfo( ""{0}"", ""default"", {{ {1} }}); }}( this )); ", name, cultureFragment); } private static string Serialize(object value) { // no need to escape single quotes return _jss.Serialize(value).Replace("\\u0027", "'"); } private static string ToJavaScript(string extend, CultureInfo culture, Dictionary dictionary, int level, bool isCalendars) { StringBuilder sb = new StringBuilder(); string padding = _padding.Substring(0, level); bool first = true; foreach (var pair in dictionary) { if (!first) { sb.Append(",\n"); } first = false; if (pair.Value is Dictionary) { sb.AppendFormat("{0}{1}: {{\n{2}\n{0}}}", padding, pair.Key, ToJavaScript(extend, culture, (Dictionary)pair.Value, level + 1, pair.Key.Equals("calendars"))); } else if (pair.Key.Equals("convert")) { sb.AppendFormat("{0}convert: {{\n{1}\n{0}}}", padding, pair.Value); } else if (pair.Key.Equals("groupSeparator")) { sb.AppendFormat("{0}\",\": {1}", padding, Serialize(pair.Value)); } else if (pair.Key.Equals("decimalSeparator")) { sb.AppendFormat("{0}\".\": {1}", padding, Serialize(pair.Value)); } else if (pair.Key.Equals("positive")) { sb.AppendFormat("{0}\"+\": {1}", padding, Serialize(pair.Value)); } else if (pair.Key.Equals("negative")) { sb.AppendFormat("{0}\"-\": {1}", padding, Serialize(pair.Value)); } else if (pair.Key.Equals("dateSeparator")) { sb.AppendFormat("{0}\"/\": {1}", padding, Serialize(pair.Value)); } else if (pair.Key.Equals("timeSeparator")) { sb.AppendFormat("{0}\":\": {1}", padding, Serialize(pair.Value)); } else { sb.AppendFormat("{0}{1}: {2}", padding, pair.Key, Serialize(pair.Value)); } } return sb.ToString(); } private static JavaScriptSerializer _jss = new JavaScriptSerializer(); private static string _padding = " "; private static Dictionary ToDictionary(IEnumerable> pairs) { var d = new Dictionary(); foreach (var pair in pairs) { d.Add(pair.Key, pair.Value); } return d; } public static Dictionary DiffGlobInfos(Dictionary glob1, Dictionary glob2) { var unique = new Dictionary(); var comparer = new KeyValueComparer(); var diff = ToDictionary(glob2.Except(glob1, comparer)); foreach (var pair in glob2) { if (diff.ContainsKey(pair.Key) && pair.Value != null) { if (pair.Value is Dictionary) { var subdiff = glob1.ContainsKey(pair.Key) ? DiffGlobInfos((Dictionary)glob1[pair.Key], (Dictionary)pair.Value) : (Dictionary)pair.Value; if (subdiff.Count > 0) { //Debug.WriteLine("Replacing\n {0}\nwith\n {1}", _jss.Serialize(diff[pair.Key]), _jss.Serialize(subdiff)); diff[pair.Key] = subdiff; } else { //Debug.WriteLine("\nRemoving {0}\n", _jss.Serialize(pair.Key)); diff.Remove(pair.Key); } } else if (pair.Value is IList) { if (glob1.ContainsKey(pair.Key) && ListEquality((IList)pair.Value, (IList)glob1[pair.Key])) { diff.Remove(pair.Key); } } } } return diff; } public class KeyValueComparer : IEqualityComparer> { public bool Equals(KeyValuePair x, KeyValuePair y) { if (x.Key.Equals(y.Key)) { if ((x.Value == null && y.Value == null) || (x.Value != null && x.Value.Equals(y.Value))) { return true; } else if (x.Value is ArrayList && y.Value is ArrayList) { return ListEquality((IList)x.Value, (IList)y.Value); } else if (x.Value is Array && y.Value is Array) { return ArrayEquality(x.Value as Array, y.Value as Array); } else if (x.Value is Dictionary && y.Value is Dictionary) { var diff = DiffGlobInfos((Dictionary)x.Value, (Dictionary)y.Value); if (diff.Count == 0) { return true; } //else { // Debug.WriteLine(" Dictionaries diff:\n {0}\n {1}", _jss.Serialize(x.Value), _jss.Serialize(y.Value)); //} } } //Debug.WriteLine(" Diff found: {0}={1}, {2}={3}", x.Key, x.Value, y.Key, y.Value); return false; } public int GetHashCode(KeyValuePair obj) { return obj.GetHashCode(); } } public class NumberFormatInfo { public string[] pattern; public int decimals; public string groupSeparator; public string decimalSeparator; public int[] groupSizes; public string NaN; public string negative; public string negativeInfinity; public string positive; public string positiveInfinity; public NumberClassFormatInfo percent; public NumberClassFormatInfo currency; public class NumberClassFormatInfo { public string[] pattern; public int decimals; public int[] groupSizes; public string groupSeparator; public string decimalSeparator; public string symbol; } } public class DateFormatInfo { public string name; public string dateSeparator; public string timeSeparator; public int firstDay; public DayInfo days; public MonthInfo months; public MonthInfo monthsGenitive; public string[] AM; public string[] PM; public EraInfo[] eras; public int twoDigitYearMax; public Dictionary patterns; internal string convertScriptBlock; public class EraInfo { public string name; public long? start; public long offset; } public class MonthInfo { public string[] names; public string[] namesAbbr; } public class DayInfo { public string[] names; public string[] namesAbbr; public string[] namesShort; } } } public class Program { private static void WriteCulture(string outputdir, string fileName, string extend, string global, CultureInfo culture, StringBuilder aggregateScript) { var globInfo = GlobalizationInfo.GetGlobInfo(culture); var diff = (String.IsNullOrEmpty(extend) || culture == CultureInfo.InvariantCulture || culture.Name.Equals("en")) ? globInfo.ToDictionary(false) : GlobalizationInfo.DiffGlobInfos(GlobalizationInfo.BasisGlobInfo, globInfo.ToDictionary(true)); // Fix for Issue #31 - en-US 'englishName' is wrong // Special case diff of englishName for en-US. The generator diff seemingly finds both "en" and "en-US" to // have englishName "English (United States)" but globalize.js (correctly) has the neutral "English" for "en"/"default" if (culture.Name.Equals("en-US")) { diff.Add("name", globInfo.name); diff.Add("englishName", globInfo.englishName); } var script = GlobalizationInfo.GenerateJavaScript(extend, global, culture, culture.Name, diff, aggregateScript); var filePath = Path.Combine(outputdir, String.Format(fileName, (String.IsNullOrEmpty(culture.Name) ? "invariant" : culture.Name))); File.WriteAllText(filePath, script); Console.WriteLine(filePath); } [STAThread] static void Main(string[] args) { string outputdir = "lib\\cultures"; string extend = "extend"; string global = "Globalization"; string fileName = "globalize.culture.{0}.js"; string aggregateFileName = "globalize.cultures.js"; foreach (string param in string.Join(" ", args).SplitCommandLine()) { if (param.StartsWith("/o:")) { outputdir = param.Substring("/o:".Length); } else if (param == "/?") { Console.Write(@" Usage:glob-generator [] options: /o: The directory to put the culture scripts into. The directory will be created if it does not exist. Existing scripts there will be overwritten if necessary. default: ""lib\cultures"" "); return; } } Directory.CreateDirectory(outputdir); GlobalizationInfo.BasisGlobInfo = GlobalizationInfo.GetGlobInfo(CultureInfo.CreateSpecificCulture("en")).ToDictionary(false); StringBuilder aggregateScript = new StringBuilder(); /* Globalize.addCultureInfo( ""{0}"", ""default"", {{ {1} }}); }}( this )); " */ aggregateScript.Append( @"/* * Globalize Cultures * * http://github.com/jquery/globalize * * Copyright Software Freedom Conservancy, Inc. * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * This file was generated by the Globalize Culture Generator * Translation: bugs found in this file need to be fixed in the generator */ (function( window, undefined ) { var Globalize; if ( typeof require !== ""undefined"" && typeof exports !== ""undefined"" && typeof module !== ""undefined"" ) { // Assume CommonJS Globalize = require( ""globalize"" ); } else { // Global variable Globalize = window.Globalize; } "); int count = 0; foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures)) { if (!String.IsNullOrEmpty(culture.Name) && culture != CultureInfo.InvariantCulture && culture.Name != "en") { WriteCulture(outputdir, fileName, extend, global, culture, aggregateScript); count++; } } aggregateScript.Append("\r\n}( this ));\r\n"); string aggregateScriptString = aggregateScript.ToString(); string aggregatePath = Path.Combine(outputdir, aggregateFileName); File.WriteAllText(aggregatePath, aggregateScriptString); Console.WriteLine(aggregatePath); Console.WriteLine("Done! Generated scripts for a total of {0} cultures, and 1 aggregate script.", count); } } }