5 // Jackson Harper (jackson@ximian.com)
6 // Atsushi Enomoto (atsushi@ximian.com)
7 // Marek Safar <marek.safar@gmail.com>
9 // (C) 2004-2005 Novell, Inc (http://www.novell.com)
10 // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
36 using System.Globalization;
37 using System.Text.RegularExpressions;
38 using System.Collections.Generic;
41 namespace Mono.Tools.LocaleBuilder
45 static readonly string data_root = Path.Combine ("CLDR", "common");
47 public static void Main (string[] args)
49 Driver d = new Driver ();
54 private static void ParseArgs (string[] args, Driver d)
56 for (int i = 0; i < args.Length; i++) {
57 if (args[i] == "--lang" && i + 1 < args.Length)
59 else if (args[i] == "--locales" && i + 1 < args.Length)
60 d.Locales = args[++i];
61 else if (args[i] == "--header" && i + 1 < args.Length)
62 d.HeaderFileName = args[++i];
63 else if (args[i] == "--compare")
64 d.OutputCompare = true;
69 private string locales;
70 private string header_name;
71 List<CultureInfoEntry> cultures;
72 Dictionary<string, string> region_currency;
73 Dictionary<string, string> currency_fractions;
75 // The lang is the language that display names will be displayed in
89 get { return locales; }
90 set { locales = value; }
93 public string HeaderFileName
97 if (header_name == null)
98 return "culture-info-tables.h";
101 set { header_name = value; }
104 public bool OutputCompare { get; set; }
108 cultures.Sort ((a, b) => int.Parse (a.LCID.Substring (2), NumberStyles.HexNumber).CompareTo (int.Parse (b.LCID.Substring (2), NumberStyles.HexNumber)));
110 var writer = Console.Out;
112 foreach (var c in cultures) {
113 writer.WriteLine ("Name: {0}, LCID {1}", c.OriginalName, c.LCID);
115 writer.WriteLine ("{0}: {1}", "DisplayName", c.DisplayName);
116 writer.WriteLine ("{0}: {1}", "EnglishName", c.EnglishName);
117 writer.WriteLine ("{0}: {1}", "NativeName", c.NativeName);
118 // writer.WriteLine ("{0}: {1}", "OptionalCalendars", c.OptionalCalendars);
119 writer.WriteLine ("{0}: {1}", "ThreeLetterISOLanguageName", c.ThreeLetterISOLanguageName);
120 writer.WriteLine ("{0}: {1}", "ThreeLetterWindowsLanguageName", c.ThreeLetterWindowsLanguageName);
121 writer.WriteLine ("{0}: {1}", "TwoLetterISOLanguageName", c.TwoLetterISOLanguageName);
122 writer.WriteLine ("{0}: {1}", "Calendar", GetCalendarType (c.CalendarType));
124 var df = c.DateTimeFormatEntry;
125 writer.WriteLine ("-- DateTimeFormat --");
126 Dump (writer, df.AbbreviatedDayNames, "AbbreviatedDayNames");
127 Dump (writer, df.AbbreviatedMonthGenitiveNames, "AbbreviatedMonthGenitiveNames");
128 Dump (writer, df.AbbreviatedMonthNames, "AbbreviatedMonthNames");
129 writer.WriteLine ("{0}: {1}", "AMDesignator", df.AMDesignator);
130 writer.WriteLine ("{0}: {1}", "CalendarWeekRule", (CalendarWeekRule) df.CalendarWeekRule);
131 writer.WriteLine ("{0}: {1}", "DateSeparator", df.DateSeparator);
132 Dump (writer, df.DayNames, "DayNames");
133 writer.WriteLine ("{0}: {1}", "FirstDayOfWeek", (DayOfWeek) df.FirstDayOfWeek);
134 // Dump (writer, df.GetAllDateTimePatterns (), "GetAllDateTimePatterns");
135 writer.WriteLine ("{0}: {1}", "LongDatePattern", df.LongDatePattern);
136 writer.WriteLine ("{0}: {1}", "LongTimePattern", df.LongTimePattern);
137 writer.WriteLine ("{0}: {1}", "MonthDayPattern", df.MonthDayPattern);
138 Dump (writer, df.MonthGenitiveNames, "MonthGenitiveNames");
139 Dump (writer, df.MonthNames, "MonthNames");
140 writer.WriteLine ("{0}: {1}", "NativeCalendarName", df.NativeCalendarName);
141 writer.WriteLine ("{0}: {1}", "PMDesignator", df.PMDesignator);
142 writer.WriteLine ("{0}: {1}", "ShortDatePattern", df.ShortDatePattern);
143 Dump (writer, df.ShortestDayNames, "ShortestDayNames");
144 writer.WriteLine ("{0}: {1}", "ShortTimePattern", df.ShortTimePattern);
145 writer.WriteLine ("{0}: {1}", "TimeSeparator", df.TimeSeparator);
146 writer.WriteLine ("{0}: {1}", "YearMonthPattern", df.YearMonthPattern);
148 var ti = c.TextInfoEntry;
149 writer.WriteLine ("-- TextInfo --");
150 writer.WriteLine ("{0}: {1}", "ANSICodePage", ti.ANSICodePage);
151 writer.WriteLine ("{0}: {1}", "EBCDICCodePage", ti.EBCDICCodePage);
152 writer.WriteLine ("{0}: {1}", "IsRightToLeft", ti.IsRightToLeft);
153 writer.WriteLine ("{0}: {1}", "ListSeparator", ti.ListSeparator);
154 writer.WriteLine ("{0}: {1}", "MacCodePage", ti.MacCodePage);
155 writer.WriteLine ("{0}: {1}", "OEMCodePage", ti.OEMCodePage);
157 var nf = c.NumberFormatEntry;
158 writer.WriteLine ("-- NumberFormat --");
159 writer.WriteLine ("{0}: {1}", "CurrencyDecimalDigits", nf.CurrencyDecimalDigits);
160 writer.WriteLine ("{0}: {1}", "CurrencyDecimalSeparator", nf.CurrencyDecimalSeparator);
161 writer.WriteLine ("{0}: {1}", "CurrencyGroupSeparator", nf.CurrencyGroupSeparator);
162 Dump (writer, nf.CurrencyGroupSizes, "CurrencyGroupSizes", true);
163 writer.WriteLine ("{0}: {1}", "CurrencyNegativePattern", nf.CurrencyNegativePattern);
164 writer.WriteLine ("{0}: {1}", "CurrencyPositivePattern", nf.CurrencyPositivePattern);
165 writer.WriteLine ("{0}: {1}", "CurrencySymbol", nf.CurrencySymbol);
166 writer.WriteLine ("{0}: {1}", "DigitSubstitution", nf.DigitSubstitution);
167 writer.WriteLine ("{0}: {1}", "NaNSymbol", nf.NaNSymbol);
168 Dump (writer, nf.NativeDigits, "NativeDigits");
169 writer.WriteLine ("{0}: {1}", "NegativeInfinitySymbol", nf.NegativeInfinitySymbol);
170 writer.WriteLine ("{0}: {1}", "NegativeSign", nf.NegativeSign);
171 writer.WriteLine ("{0}: {1}", "NumberDecimalDigits", nf.NumberDecimalDigits);
172 writer.WriteLine ("{0}: {1}", "NumberDecimalSeparator", nf.NumberDecimalSeparator);
173 writer.WriteLine ("{0}: {1}", "NumberGroupSeparator", nf.NumberGroupSeparator);
174 Dump (writer, nf.NumberGroupSizes, "NumberGroupSizes", true);
175 writer.WriteLine ("{0}: {1}", "NumberNegativePattern", nf.NumberNegativePattern);
176 writer.WriteLine ("{0}: {1}", "PercentDecimalDigits", nf.PercentDecimalDigits);
177 writer.WriteLine ("{0}: {1}", "PercentDecimalSeparator", nf.PercentDecimalSeparator);
178 writer.WriteLine ("{0}: {1}", "PercentGroupSeparator", nf.PercentGroupSeparator);
179 Dump (writer, nf.PercentGroupSizes, "PercentGroupSizes", true);
180 writer.WriteLine ("{0}: {1}", "PercentNegativePattern", nf.PercentNegativePattern);
181 writer.WriteLine ("{0}: {1}", "PercentPositivePattern", nf.PercentPositivePattern);
182 writer.WriteLine ("{0}: {1}", "PercentSymbol", nf.PercentSymbol);
183 writer.WriteLine ("{0}: {1}", "PerMilleSymbol", nf.PerMilleSymbol);
184 writer.WriteLine ("{0}: {1}", "PositiveInfinitySymbol", nf.PositiveInfinitySymbol);
185 writer.WriteLine ("{0}: {1}", "PositiveSign", nf.PositiveSign);
187 if (c.RegionInfoEntry != null) {
188 var ri = c.RegionInfoEntry;
189 writer.WriteLine ("-- RegionInfo --");
190 writer.WriteLine ("{0}: {1}", "CurrencyEnglishName", ri.CurrencyEnglishName);
191 writer.WriteLine ("{0}: {1}", "CurrencyNativeName", ri.CurrencyNativeName);
192 writer.WriteLine ("{0}: {1}", "CurrencySymbol", ri.CurrencySymbol);
193 writer.WriteLine ("{0}: {1}", "DisplayName", ri.DisplayName);
194 writer.WriteLine ("{0}: {1}", "EnglishName", ri.EnglishName);
195 writer.WriteLine ("{0}: {1}", "GeoId", ri.GeoId);
196 writer.WriteLine ("{0}: {1}", "IsMetric", ri.IsMetric);
197 writer.WriteLine ("{0}: {1}", "ISOCurrencySymbol", ri.ISOCurrencySymbol);
198 writer.WriteLine ("{0}: {1}", "Name", ri.Name);
199 writer.WriteLine ("{0}: {1}", "NativeName", ri.NativeName);
200 writer.WriteLine ("{0}: {1}", "ThreeLetterISORegionName", ri.ThreeLetterISORegionName);
201 writer.WriteLine ("{0}: {1}", "ThreeLetterWindowsRegionName", ri.ThreeLetterWindowsRegionName);
202 writer.WriteLine ("{0}: {1}", "TwoLetterISORegionName", ri.TwoLetterISORegionName);
209 static Type GetCalendarType (CalendarType ct)
212 case CalendarType.Gregorian:
213 return typeof (GregorianCalendar);
214 case CalendarType.HijriCalendar:
215 return typeof (HijriCalendar);
216 case CalendarType.ThaiBuddhist:
217 return typeof (ThaiBuddhistCalendar);
218 case CalendarType.UmAlQuraCalendar:
219 return typeof (UmAlQuraCalendar);
221 throw new NotImplementedException ();
225 static void Dump<T> (TextWriter tw, IList<T> values, string name, bool stopOnNull = false) where T : class
230 for (int i = 0; i < values.Count; ++i) {
233 if (stopOnNull && v == null)
247 Regex locales_regex = null;
249 locales_regex = new Regex (Locales);
251 cultures = new List<CultureInfoEntry> ();
252 var regions = new List<RegionInfoEntry> ();
255 var supplemental = GetXmlDocument (Path.Combine (data_root, "supplemental", "supplementalData.xml"));
257 // Read currencies info
258 region_currency = new Dictionary<string, string> (StringComparer.OrdinalIgnoreCase);
259 foreach (XmlNode entry in supplemental.SelectNodes ("supplementalData/currencyData/region")) {
260 var child = entry.SelectSingleNode ("currency");
261 region_currency.Add (entry.Attributes["iso3166"].Value, child.Attributes["iso4217"].Value);
264 var lcdids = GetXmlDocument ("lcids.xml");
265 foreach (XmlNode lcid in lcdids.SelectNodes ("lcids/lcid")) {
266 var name = lcid.Attributes["name"].Value;
268 if (locales_regex != null && !locales_regex.IsMatch (name))
271 var ci = new CultureInfoEntry ();
272 ci.LCID = lcid.Attributes["id"].Value;
273 ci.ParentLcid = lcid.Attributes["parent"].Value;
274 ci.TwoLetterISOLanguageName = lcid.Attributes["iso2"].Value;
275 ci.ThreeLetterISOLanguageName = lcid.Attributes["iso3"].Value;
276 ci.ThreeLetterWindowsLanguageName = lcid.Attributes["win"].Value;
277 ci.OriginalName = name.Replace ('_', '-');
278 ci.TextInfoEntry = new TextInfoEntry ();
279 ci.NumberFormatEntry = new NumberFormatEntry ();
281 if (!Import (ci, name))
287 var doc_english = GetXmlDocument (Path.Combine (data_root, "main", "en.xml"));
290 // Fill all EnglishName values from en.xml language file
292 foreach (var ci in cultures) {
293 var el = doc_english.SelectSingleNode (string.Format ("ldml/localeDisplayNames/languages/language[@type='{0}']", ci.Language));
295 ci.EnglishName = el.InnerText;
298 if (ci.Script != null) {
299 el = doc_english.SelectSingleNode (string.Format ("ldml/localeDisplayNames/scripts/script[@type='{0}']", ci.Script));
304 if (ci.Territory != null) {
305 el = doc_english.SelectSingleNode (string.Format ("ldml/localeDisplayNames/territories/territory[@type='{0}']", ci.Territory));
310 s = string.Join (", ", s, el.InnerText);
314 switch (ci.ThreeLetterWindowsLanguageName) {
324 ci.EnglishName = string.Format ("{0} ({1})", ci.EnglishName, s);
326 // Special case legacy chinese
327 if (ci.OriginalName == "zh-CHS" || ci.OriginalName == "zh-CHT")
328 ci.EnglishName += " Legacy";
330 // Mono is not localized and supports english only, hence the name will always be same
331 ci.DisplayName = ci.EnglishName;
335 // Fill culture hierarchy for easier data manipulation
337 foreach (var ci in cultures) {
338 foreach (var p in cultures.Where (l => ci.LCID == l.ParentLcid)) {
343 currency_fractions = new Dictionary<string, string> (StringComparer.OrdinalIgnoreCase);
344 foreach (XmlNode entry in supplemental.SelectNodes ("supplementalData/currencyData/fractions/info")) {
345 currency_fractions.Add (entry.Attributes["iso4217"].Value, entry.Attributes["digits"].Value);
348 var territory2dayofweek = new Dictionary<string, DayOfWeek> (StringComparer.OrdinalIgnoreCase);
349 foreach (XmlNode entry in supplemental.SelectNodes ("supplementalData/weekData/firstDay")) {
352 switch (entry.Attributes["day"].Value) {
354 dow = DayOfWeek.Monday;
357 dow = DayOfWeek.Friday;
360 dow = DayOfWeek.Saturday;
363 dow = DayOfWeek.Sunday;
366 throw new NotImplementedException ();
369 var territories = entry.Attributes["territories"].Value.Split ();
370 foreach (var t in territories)
371 territory2dayofweek[t] = dow;
374 var territory2wr = new Dictionary<string, CalendarWeekRule> (StringComparer.OrdinalIgnoreCase);
375 foreach (XmlNode entry in supplemental.SelectNodes ("supplementalData/weekData/minDays")) {
376 CalendarWeekRule rule;
378 switch (entry.Attributes["count"].InnerText) {
380 rule = CalendarWeekRule.FirstDay;
383 rule = CalendarWeekRule.FirstFourDayWeek;
386 throw new NotImplementedException ();
389 var territories = entry.Attributes["territories"].InnerText.Split ();
390 foreach (var t in territories)
391 territory2wr[t] = rule;
395 // Fill all territory speficic data where territory is available
397 var non_metric = new HashSet<string> ();
398 foreach (XmlNode entry in supplemental.SelectNodes ("supplementalData/measurementData/measurementSystem[@type='US']")) {
399 var territories = entry.Attributes["territories"].InnerText.Split ();
400 foreach (var t in territories)
404 foreach (var ci in cultures) {
405 if (ci.Territory == null)
409 if (territory2dayofweek.TryGetValue (ci.Territory, out value)) {
410 ci.DateTimeFormatEntry.FirstDayOfWeek = (int) value;
413 CalendarWeekRule rule;
414 if (territory2wr.TryGetValue (ci.Territory, out rule)) {
415 ci.DateTimeFormatEntry.CalendarWeekRule = (int) rule;
418 string fraction_value;
419 if (currency_fractions.TryGetValue (ci.Territory, out fraction_value)) {
420 ci.NumberFormatEntry.CurrencyDecimalDigits = fraction_value;
423 RegionInfoEntry region = regions.Where (l => l.Name == ci.Territory).FirstOrDefault ();
424 if (region == null) {
425 region = new RegionInfoEntry () {
426 CurrencySymbol = ci.NumberFormatEntry.CurrencySymbol,
427 EnglishName = ci.EnglishName,
428 NativeName = ci.NativeTerritoryName,
430 TwoLetterISORegionName = ci.Territory,
431 CurrencyNativeName = ci.NativeCurrencyName
434 var tc = supplemental.SelectSingleNode (string.Format ("supplementalData/codeMappings/territoryCodes[@type='{0}']", ci.Territory));
435 region.ThreeLetterISORegionName = tc.Attributes["alpha3"].Value;
436 region.ThreeLetterWindowsRegionName = region.ThreeLetterISORegionName;
438 var el = doc_english.SelectSingleNode (string.Format ("ldml/localeDisplayNames/territories/territory[@type='{0}']", ci.Territory));
439 region.EnglishName = el.InnerText;
440 region.DisplayName = region.EnglishName;
442 region.ISOCurrencySymbol = region_currency[ci.Territory];
444 el = doc_english.SelectSingleNode (string.Format ("ldml/numbers/currencies/currency[@type='{0}']/displayName", region.ISOCurrencySymbol));
445 region.CurrencyEnglishName = el.InnerText;
447 if (non_metric.Contains (ci.Territory))
448 region.IsMetric = false;
450 var lcdid_value = int.Parse (ci.LCID.Substring (2), NumberStyles.HexNumber);
451 Patterns.FillValues (lcdid_value, region);
452 regions.Add (region);
455 ci.RegionInfoEntry = region;
459 // Fill neutral cultures territory data
461 foreach (var ci in cultures) {
462 var dtf = ci.DateTimeFormatEntry;
463 if (dtf.FirstDayOfWeek == null) {
466 dtf.FirstDayOfWeek = (int) DayOfWeek.Saturday;
471 dtf.FirstDayOfWeek = (int) DayOfWeek.Sunday;
478 dtf.FirstDayOfWeek = (int) DayOfWeek.Monday;
481 List<int?> all_fdow = new List<int?> ();
482 GetAllChildrenValues (ci, all_fdow, l => l.DateTimeFormatEntry.FirstDayOfWeek);
483 var children = all_fdow.Where (l => l != null).Distinct ().ToList ();
485 if (children.Count == 1) {
486 dtf.FirstDayOfWeek = children[0];
487 } else if (children.Count == 0) {
488 if (!ci.HasMissingLocale)
489 Console.WriteLine ("No week data for `{0}'", ci.Name);
492 dtf.FirstDayOfWeek = (int) DayOfWeek.Sunday;
494 // .NET has weird concept of territory data available for neutral cultures (e.g. en, es, pt)
495 // We have to manually disambiguate the correct entry (which is artofficial anyway)
496 throw new ApplicationException (string.Format ("Ambiguous week data for `{0}'", ci.Name));
503 if (dtf.CalendarWeekRule == null) {
512 dtf.CalendarWeekRule = (int) CalendarWeekRule.FirstDay;
515 List<int?> all_cwr = new List<int?> ();
516 GetAllChildrenValues (ci, all_cwr, l => l.DateTimeFormatEntry.CalendarWeekRule);
517 var children = all_cwr.Where (l => l != null).Distinct ().ToList ();
519 if (children.Count == 1) {
520 dtf.CalendarWeekRule = children[0];
521 } else if (children.Count == 0) {
522 if (!ci.HasMissingLocale)
523 Console.WriteLine ("No calendar week data for `{0}'", ci.Name);
526 // Default to FirstDay
527 dtf.CalendarWeekRule = (int) CalendarWeekRule.FirstDay;
529 // .NET has weird concept of territory data available for neutral cultures (e.g. en, es, pt)
530 // We have to manually disambiguate the correct entry (which is artofficial anyway)
531 throw new ApplicationException (string.Format ("Ambiguous calendar data for `{0}'", ci.Name));
538 var nfe = ci.NumberFormatEntry;
539 if (nfe.CurrencySymbol == null) {
542 nfe.CurrencySymbol = "ر.س.";
545 nfe.CurrencySymbol = "$";
549 nfe.CurrencySymbol = "€";
552 nfe.CurrencySymbol = "R$";
555 nfe.CurrencySymbol = "kr";
558 nfe.CurrencySymbol = "RM";
561 nfe.CurrencySymbol = "টা";
564 nfe.CurrencySymbol = "Дин.";
568 nfe.CurrencySymbol = "Din.";
571 nfe.CurrencySymbol = "¥";
574 nfe.CurrencySymbol = "HK$";
578 var all_currencies = new List<string> ();
579 GetAllChildrenValues (ci, all_currencies, l => l.NumberFormatEntry.CurrencySymbol);
580 var children = all_currencies.Where (l => l != null).Distinct ().ToList ();
582 if (children.Count == 1) {
583 nfe.CurrencySymbol = children[0];
584 } else if (children.Count == 0) {
585 if (!ci.HasMissingLocale)
586 Console.WriteLine ("No currency data for `{0}'", ci.Name);
590 // .NET has weird concept of territory data available for neutral cultures (e.g. en, es, pt)
591 // We have to manually disambiguate the correct entry (which is artofficial anyway)
592 throw new ApplicationException (string.Format ("Ambiguous currency data for `{0}'", ci.Name));
599 if (nfe.CurrencyDecimalDigits == null) {
600 var all_digits = new List<string> ();
601 GetAllChildrenValues (ci, all_digits, l => l.NumberFormatEntry.CurrencyDecimalDigits);
602 var children = all_digits.Where (l => l != null).Distinct ().ToList ();
604 if (children.Count == 1) {
605 nfe.CurrencyDecimalDigits = children[0];
606 } else if (children.Count == 0) {
607 if (!ci.HasMissingLocale)
608 Console.WriteLine ("No currency decimal digits data for `{0}'", ci.Name);
610 nfe.CurrencyDecimalDigits = "2";
612 // .NET has weird concept of territory data available for neutral cultures (e.g. en, es, pt)
613 // We have to manually disambiguate the correct entry (which is artofficial anyway)
614 throw new ApplicationException (string.Format ("Ambiguous currency decimal digits data for `{0}'", ci.Name));
622 regions.Sort (new RegionComparer ());
623 for (int i = 0; i < regions.Count; ++i)
624 regions[i].Index = i;
627 * Dump each table individually. Using StringBuilders
628 * because it is easier to debug, should switch to just
629 * writing to streams eventually.
631 using (StreamWriter writer = new StreamWriter (HeaderFileName, false, new UTF8Encoding (false, true))) {
632 writer.NewLine = "\n";
634 writer.WriteLine ("/* This is a generated file. Do not edit. See tools/locale-builder. */");
635 writer.WriteLine ("#ifndef MONO_METADATA_CULTURE_INFO_TABLES");
636 writer.WriteLine ("#define MONO_METADATA_CULTURE_INFO_TABLES 1");
637 writer.WriteLine ("\n");
639 writer.WriteLine ("#define NUM_CULTURE_ENTRIES {0}", cultures.Count);
640 writer.WriteLine ("#define NUM_REGION_ENTRIES {0}", regions.Count);
642 writer.WriteLine ("\n");
644 // Sort the cultures by lcid
645 cultures.Sort (new LcidComparer ());
647 StringBuilder builder = new StringBuilder ();
649 int count = cultures.Count;
650 for (int i = 0; i < count; i++) {
651 CultureInfoEntry ci = cultures[i];
652 if (ci.DateTimeFormatEntry == null)
654 ci.DateTimeFormatEntry.AppendTableRow (builder);
655 ci.DateTimeFormatEntry.Row = row++;
657 builder.Append (',');
658 builder.Append ('\n');
661 writer.WriteLine ("static const DateTimeFormatEntry datetime_format_entries [] = {");
662 writer.Write (builder);
663 writer.WriteLine ("};\n\n");
665 builder = new StringBuilder ();
667 for (int i = 0; i < count; i++) {
668 CultureInfoEntry ci = cultures[i];
669 if (ci.NumberFormatEntry == null)
671 ci.NumberFormatEntry.AppendTableRow (builder);
672 ci.NumberFormatEntry.Row = row++;
674 builder.Append (',');
675 builder.Append ('\n');
678 writer.WriteLine ("static const NumberFormatEntry number_format_entries [] = {");
679 writer.Write (builder);
680 writer.WriteLine ("};\n\n");
682 builder = new StringBuilder ();
684 for (int i = 0; i < count; i++) {
685 CultureInfoEntry ci = cultures[i];
686 ci.AppendTableRow (builder);
689 builder.Append (',');
690 builder.Append ('\n');
693 writer.WriteLine ("static const CultureInfoEntry culture_entries [] = {");
694 writer.Write (builder);
695 writer.WriteLine ("};\n\n");
697 cultures.Sort (new ExportNameComparer ()); // Sort based on name
698 builder = new StringBuilder ();
699 for (int i = 0; i < count; i++) {
700 CultureInfoEntry ci = cultures[i];
701 var name = ci.GetExportName ().ToLowerInvariant ();
702 builder.Append ("\t{" + Entry.EncodeStringIdx (name) + ", ");
703 builder.Append (ci.Row + "}");
705 builder.Append (',');
707 builder.AppendFormat ("\t /* {0} */", name);
708 builder.Append ('\n');
711 writer.WriteLine ("static const CultureInfoNameEntry culture_name_entries [] = {");
712 writer.Write (builder);
713 writer.WriteLine ("};\n\n");
715 builder = new StringBuilder ();
717 foreach (RegionInfoEntry r in regions) {
718 r.AppendTableRow (builder);
719 if (++rcount != regions.Count)
720 builder.Append (',');
722 builder.Append ('\n');
724 writer.WriteLine ("static const RegionInfoEntry region_entries [] = {");
725 writer.Write (builder);
726 writer.WriteLine ("};\n\n");
728 builder = new StringBuilder ();
730 foreach (RegionInfoEntry ri in regions) {
731 builder.Append ("\t{" + Entry.EncodeStringIdx (ri.TwoLetterISORegionName) + ", ");
732 builder.Append (ri.Index + "}");
733 if (++rcount != regions.Count)
734 builder.Append (',');
736 builder.AppendFormat ("\t /* {0} */", ri.TwoLetterISORegionName);
737 builder.Append ('\n');
740 writer.WriteLine ("static const RegionInfoNameEntry region_name_entries [] = {");
741 writer.Write (builder);
742 writer.WriteLine ("};\n\n");
744 writer.WriteLine ("static const char locale_strings [] = {");
745 writer.Write (Entry.GetStrings ());
746 writer.WriteLine ("};\n\n");
748 writer.WriteLine ("#endif\n");
752 static void GetAllChildrenValues<T> (CultureInfoEntry entry, List<T> values, Func<CultureInfoEntry, T> selector)
754 foreach (var e in entry.Children) {
758 values.Add (selector (e));
760 foreach (var e2 in e.Children) {
761 GetAllChildrenValues (e2, values, selector);
766 static XmlDocument GetXmlDocument (string path)
768 var doc = new XmlDocument ();
769 doc.Load (new XmlTextReader (path) { /*DtdProcessing = DtdProcessing.Ignore*/ } );
773 bool Import (CultureInfoEntry data, string locale)
776 var sep = locale.Split ('_');
777 data.Language = sep[0];
779 // CLDR strictly follow ISO names, .NET does not
780 // Replace names where non-iso2 is used, e.g. Norway
781 if (data.Language != data.TwoLetterISOLanguageName) {
782 locale = data.TwoLetterISOLanguageName;
783 if (sep.Length > 1) {
784 locale += string.Join ("_", sep.Skip (1));
788 // Convert broken Chinese names to correct one
797 locale = "zh_Hans_CN";
800 locale = "zh_Hant_HK";
803 locale = "zh_Hans_SG";
806 locale = "zh_Hant_TW";
809 locale = "zh_Hant_MO";
813 sep = locale.Split ('_');
815 string full_name = Path.Combine (data_root, "main", locale + ".xml");
816 if (!File.Exists (full_name)) {
817 Console.WriteLine ("Missing locale file for `{0}'", locale);
819 // We could fill default values but that's not as simple as it seems. For instance for non-neutral
820 // cultures the next part could be territory or not.
823 XmlDocument doc = null;
826 * Locale generation is done in several steps, first we
827 * read the root file which is the base invariant data
828 * then the supplemental root data,
829 * then the language file, the supplemental languages
830 * file then the locale file, then the supplemental
831 * locale file. Values in each descending file can
832 * overwrite previous values.
834 foreach (var part in sep) {
840 var xml = GetXmlDocument (Path.Combine (data_root, "main", fname + ".xml"));
848 // Extract localized locale name from language xml file. Have to do it after both language and territory are read
850 var el = doc.SelectSingleNode (string.Format ("ldml/localeDisplayNames/languages/language[@type='{0}']", data.Language));
852 data.NativeName = el.InnerText;
854 if (data.Territory != null) {
855 el = doc.SelectSingleNode (string.Format ("ldml/localeDisplayNames/territories/territory[@type='{0}']", data.Territory));
857 // TODO: Should read <localePattern>
858 data.NativeName = string.Format ("{0} ({1})", data.NativeName, el.InnerText);
859 data.NativeTerritoryName = el.InnerText;
863 // We have territory now we have to run the process again to extract currency symbol
864 if (region_currency.TryGetValue (data.Territory, out currency)) {
867 var xml = GetXmlDocument (Path.Combine (data_root, "main", "root.xml"));
868 el = xml.SelectSingleNode (string.Format ("ldml/numbers/currencies/currency[@type='{0}']/symbol", currency));
870 data.NumberFormatEntry.CurrencySymbol = el.InnerText;
872 foreach (var part in sep) {
878 xml = GetXmlDocument (Path.Combine (data_root, "main", fname + ".xml"));
879 el = xml.SelectSingleNode (string.Format ("ldml/numbers/currencies/currency[@type='{0}']/symbol", currency));
881 data.NumberFormatEntry.CurrencySymbol = el.InnerText;
883 el = xml.SelectSingleNode (string.Format ("ldml/numbers/currencies/currency[@type='{0}']/displayName", currency));
885 data.NativeCurrencyName = el.InnerText;
890 if (data.DateTimeFormatEntry.MonthGenitiveNames[0] == null)
891 data.DateTimeFormatEntry.MonthGenitiveNames = data.DateTimeFormatEntry.MonthNames;
893 if (data.DateTimeFormatEntry.AbbreviatedMonthGenitiveNames[0] == null)
894 data.DateTimeFormatEntry.AbbreviatedMonthGenitiveNames = data.DateTimeFormatEntry.AbbreviatedMonthNames;
899 // It looks like it never changes
900 data.DateTimeFormatEntry.TimeSeparator = ":";
902 // TODO: Don't have input data available but most values are 2 with few exceptions for 1 and 3
903 // We don't add 3 as it's for some arabic states only
904 switch (data.ThreeLetterISOLanguageName) {
906 data.NumberFormatEntry.NumberDecimalDigits =
907 data.NumberFormatEntry.PercentDecimalDigits = 1;
910 data.NumberFormatEntry.NumberDecimalDigits =
911 data.NumberFormatEntry.PercentDecimalDigits = 2;
915 // TODO: For now we capture only native name for default calendar
916 data.NativeCalendarNames[((int) data.CalendarType & 0xFF) - 1] = data.DateTimeFormatEntry.NativeCalendarName;
918 var lcdid_value = int.Parse (data.LCID.Substring (2), NumberStyles.HexNumber);
919 Patterns.FillValues (lcdid_value, data);
924 void Import (XmlDocument doc, CultureInfoEntry ci)
930 // Extract script & teritory
932 el = doc.SelectSingleNode ("ldml/identity/script");
934 ci.Script = el.Attributes["type"].Value;
936 el = doc.SelectSingleNode ("ldml/identity/territory");
938 ci.Territory = el.Attributes["type"].Value;
940 var df = ci.DateTimeFormatEntry;
943 // Default calendar is for now always "gregorian"
945 case "th": case "th-TH":
946 calendar = "buddhist";
947 ci.CalendarType = CalendarType.ThaiBuddhist; // typeof (ThaiBuddhistCalendar);
949 case "ar": case "ar-SA":
950 calendar = "islamic";
951 ci.CalendarType = CalendarType.UmAlQuraCalendar; // typeof (UmAlQuraCalendar);
953 case "ps": case "ps-AF": case "prs": case "prs-AF": case "dv": case "dv-MV":
954 calendar = "persian";
955 ci.CalendarType = CalendarType.HijriCalendar; // typeof (HijriCalendar);
958 calendar = "gregorian";
959 ci.CalendarType = CalendarType.Gregorian; // typeof (GregorianCalendar);
960 ci.GregorianCalendarType = GregorianCalendarTypes.Localized;
964 var node = doc.SelectSingleNode (string.Format ("ldml/dates/calendars/calendar[@type='{0}']", calendar));
966 el = doc.SelectSingleNode (string.Format ("ldml/localeDisplayNames/types/type[@type='{0}']", calendar));
968 df.NativeCalendarName = el.InnerText;
971 // Apply global rule first <alias source="locale" path="../../monthContext[@type='format']/monthWidth[@type='wide']"/>
972 nodes = node.SelectNodes ("months/monthContext[@type='format']/monthWidth[@type='wide']/month");
973 ProcessAllNodes (nodes, df.MonthNames, AddOrReplaceValue);
974 nodes = node.SelectNodes ("months/monthContext[@type='stand-alone']/monthWidth[@type='wide']/month");
975 ProcessAllNodes (nodes, df.MonthNames, AddOrReplaceValue);
977 // Apply global rule first <alias source="locale" path="../../monthContext[@type='format']/monthWidth[@type='abbreviated']"/>
978 nodes = node.SelectNodes ("months/monthContext[@type='format']/monthWidth[@type='abbreviated']/month");
979 ProcessAllNodes (nodes, df.AbbreviatedMonthNames, AddOrReplaceValue);
980 nodes = node.SelectNodes ("months/monthContext[@type='stand-alone']/monthWidth[@type='abbreviated']/month");
981 ProcessAllNodes (nodes, df.AbbreviatedMonthNames, AddOrReplaceValue);
983 nodes = node.SelectNodes ("months/monthContext[@type='format']/monthWidth[@type='wide']/month");
985 ProcessAllNodes (nodes, df.MonthGenitiveNames, AddOrReplaceValue);
987 nodes = node.SelectNodes ("days/dayContext[@type='format']/dayWidth[@type='wide']/day");
988 ProcessAllNodes (nodes, df.DayNames, AddOrReplaceDayValue);
990 // Apply global rule first <alias source="locale" path="../../dayContext[@type='format']/dayWidth[@type='abbreviated']"/>
991 nodes = node.SelectNodes ("days/dayContext[@type='format']/dayWidth[@type='abbreviated']/day");
992 ProcessAllNodes (nodes, df.AbbreviatedDayNames, AddOrReplaceDayValue);
993 nodes = node.SelectNodes ("days/dayContext[@type='stand-alone']/dayWidth[@type='abbreviated']/day");
994 ProcessAllNodes (nodes, df.AbbreviatedDayNames, AddOrReplaceDayValue);
996 // TODO: This is not really ShortestDayNames as .NET uses it
997 // Apply global rules first <alias source="locale" path="../../dayContext[@type='stand-alone']/dayWidth[@type='narrow']"/>
998 nodes = node.SelectNodes ("days/dayContext[@type='format']/dayWidth[@type='narrow']/day");
999 ProcessAllNodes (nodes, df.ShortestDayNames, AddOrReplaceDayValue);
1000 nodes = node.SelectNodes ("days/dayContext[@type='stand-alone']/dayWidth[@type='narrow']/day");
1001 ProcessAllNodes (nodes, df.ShortestDayNames, AddOrReplaceDayValue);
1003 Cannot really be used it's too different to .NET and most app rely on it
1005 el = node.SelectSingleNode ("dateFormats/dateFormatLength[@type='full']/dateFormat/pattern");
1007 df.LongDatePattern = ConvertDatePatternFormat (el.InnerText);
1009 // Medium is our short
1010 el = node.SelectSingleNode ("dateFormats/dateFormatLength[@type='medium']/dateFormat/pattern");
1012 df.ShortDatePattern = ConvertDatePatternFormat (el.InnerText);
1014 // Medium is our Long
1015 el = node.SelectSingleNode ("timeFormats/timeFormatLength[@type='medium']/timeFormat/pattern");
1017 df.LongTimePattern = ConvertTimePatternFormat (el.InnerText);
1019 el = node.SelectSingleNode ("timeFormats/timeFormatLength[@type='short']/timeFormat/pattern");
1021 df.ShortTimePattern = ConvertTimePatternFormat (el.InnerText);
1023 el = node.SelectSingleNode ("dateTimeFormats/availableFormats/dateFormatItem[@id='yyyyMMMM']");
1025 df.YearMonthPattern = ConvertDatePatternFormat (el.InnerText);
1027 el = node.SelectSingleNode ("dateTimeFormats/availableFormats/dateFormatItem[@id='MMMMdd']");
1029 df.MonthDayPattern = ConvertDatePatternFormat (el.InnerText);
1031 el = node.SelectSingleNode ("dayPeriods/dayPeriodContext/dayPeriodWidth[@type='abbreviated']/dayPeriod[@type='am']");
1033 // Apply global rule first <alias source="locale" path="../dayPeriodWidth[@type='wide']"/>
1034 el = node.SelectSingleNode ("dayPeriods/dayPeriodContext/dayPeriodWidth[@type='wide']/dayPeriod[@type='am']");
1037 df.AMDesignator = el.InnerText;
1039 el = node.SelectSingleNode ("dayPeriods/dayPeriodContext/dayPeriodWidth[@type='abbreviated']/dayPeriod[@type='pm']");
1041 // Apply global rule first <alias source="locale" path="../dayPeriodWidth[@type='wide']"/>
1042 el = node.SelectSingleNode ("dayPeriods/dayPeriodContext/dayPeriodWidth[@type='wide']/dayPeriod[@type='pm']");
1046 df.PMDesignator = el.InnerText;
1049 var ni = ci.NumberFormatEntry;
1051 node = doc.SelectSingleNode ("ldml/numbers/symbols");
1053 el = node.SelectSingleNode ("decimal");
1055 ni.NumberDecimalSeparator =
1056 ni.PercentDecimalSeparator =
1057 ni.CurrencyDecimalSeparator = el.InnerText;
1060 el = node.SelectSingleNode ("plusSign");
1062 ni.PositiveSign = el.InnerText;
1064 el = node.SelectSingleNode ("minusSign");
1066 ni.NegativeSign = el.InnerText;
1068 el = node.SelectSingleNode ("infinity");
1070 // We cannot use the value from CLDR because many broken
1071 // .NET serializers (e.g. JSON) use text value of NegativeInfinity
1072 // and different value would break interoperability with .NET
1073 if (el != null && el.InnerText != "∞") {
1074 ni.InfinitySymbol = el.InnerText;
1077 el = node.SelectSingleNode ("perMille");
1079 ni.PerMilleSymbol = el.InnerText;
1081 el = node.SelectSingleNode ("nan");
1083 ni.NaNSymbol = el.InnerText;
1085 el = node.SelectSingleNode ("percentSign");
1087 ni.PercentSymbol = el.InnerText;
1089 el = node.SelectSingleNode ("group");
1091 ni.NumberGroupSeparator =
1092 ni.PercentGroupSeparator =
1093 ni.CurrencyGroupSeparator = el.InnerText;
1098 static string ConvertDatePatternFormat (string format)
1101 // LDMR uses different characters for some fields
1102 // http://unicode.org/reports/tr35/#Date_Format_Patterns
1104 format = format.Replace ("EEEE", "dddd"); // The full name of the day of the week
1105 format = format.Replace ("LLLL", "MMMM"); // The full month name
1107 if (format.EndsWith (" y", StringComparison.Ordinal))
1113 static string ConvertTimePatternFormat (string format)
1115 format = format.Replace ("a", "tt"); // AM or PM
1119 static void ProcessAllNodes (XmlNodeList list, IList<string> values, Action<IList<string>, string, string> convertor)
1121 foreach (XmlNode entry in list) {
1122 var index = entry.Attributes["type"].Value;
1123 var value = entry.InnerText;
1124 convertor (values, index, value);
1128 // All text indexes are 1-based
1129 static void AddOrReplaceValue (IList<string> list, string oneBasedIndex, string value)
1131 int index = int.Parse (oneBasedIndex);
1132 AddOrReplaceValue (list, index - 1, value);
1135 static readonly string[] day_types = new string[] { "sun", "mon", "tue", "wed", "thu", "fri", "sat" };
1137 static void AddOrReplaceDayValue (IList<string> list, string dayType, string value)
1139 int index = Array.IndexOf (day_types, dayType);
1140 AddOrReplaceValue (list, index, value);
1143 static void AddOrReplaceValue (IList<string> list, int index, string value)
1145 if (list.Count <= index)
1146 ((List<string>) list).AddRange (new string[index - list.Count + 1]);
1148 list[index] = value;
1151 sealed class LcidComparer : IComparer<CultureInfoEntry>
1153 public int Compare (CultureInfoEntry x, CultureInfoEntry y)
1155 return x.LCID.CompareTo (y.LCID);
1159 sealed class ExportNameComparer : IComparer<CultureInfoEntry>
1161 public int Compare (CultureInfoEntry x, CultureInfoEntry y)
1163 return String.Compare (x.GetExportName (), y.GetExportName (), StringComparison.OrdinalIgnoreCase);
1167 class RegionComparer : IComparer<RegionInfoEntry>
1169 public int Compare (RegionInfoEntry x, RegionInfoEntry y)
1171 return x.TwoLetterISORegionName.CompareTo (y.TwoLetterISORegionName);