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
;
74 Dictionary
<string, string> extra_parent_locales
;
76 // The lang is the language that display names will be displayed in
90 get { return locales; }
91 set { locales = value; }
94 public string HeaderFileName
98 if (header_name
== null)
99 return "culture-info-tables.h";
102 set { header_name = value; }
105 public bool OutputCompare { get; set; }
109 cultures
.Sort ((a
, b
) => int.Parse (a
.LCID
.Substring (2), NumberStyles
.HexNumber
).CompareTo (int.Parse (b
.LCID
.Substring (2), NumberStyles
.HexNumber
)));
111 var writer
= Console
.Out
;
113 foreach (var c
in cultures
) {
114 writer
.WriteLine ("Name: {0}, LCID {1}", c
.OriginalName
, c
.LCID
);
116 writer
.WriteLine ("{0}: {1}", "DisplayName", c
.DisplayName
);
117 writer
.WriteLine ("{0}: {1}", "EnglishName", c
.EnglishName
);
118 writer
.WriteLine ("{0}: {1}", "NativeName", c
.NativeName
);
119 // writer.WriteLine ("{0}: {1}", "OptionalCalendars", c.OptionalCalendars);
120 writer
.WriteLine ("{0}: {1}", "ThreeLetterISOLanguageName", c
.ThreeLetterISOLanguageName
);
121 writer
.WriteLine ("{0}: {1}", "ThreeLetterWindowsLanguageName", c
.ThreeLetterWindowsLanguageName
);
122 writer
.WriteLine ("{0}: {1}", "TwoLetterISOLanguageName", c
.TwoLetterISOLanguageName
);
123 writer
.WriteLine ("{0}: {1}", "Calendar", GetCalendarType (c
.CalendarType
));
125 var df
= c
.DateTimeFormatEntry
;
126 writer
.WriteLine ("-- DateTimeFormat --");
127 Dump (writer
, df
.AbbreviatedDayNames
, "AbbreviatedDayNames");
128 Dump (writer
, df
.AbbreviatedMonthGenitiveNames
, "AbbreviatedMonthGenitiveNames");
129 Dump (writer
, df
.AbbreviatedMonthNames
, "AbbreviatedMonthNames");
130 writer
.WriteLine ("{0}: {1}", "AMDesignator", df
.AMDesignator
);
131 writer
.WriteLine ("{0}: {1}", "CalendarWeekRule", (CalendarWeekRule
) df
.CalendarWeekRule
);
132 writer
.WriteLine ("{0}: {1}", "DateSeparator", df
.DateSeparator
);
133 Dump (writer
, df
.DayNames
, "DayNames");
134 writer
.WriteLine ("{0}: {1}", "FirstDayOfWeek", (DayOfWeek
) df
.FirstDayOfWeek
);
135 // Dump (writer, df.GetAllDateTimePatterns (), "GetAllDateTimePatterns");
136 writer
.WriteLine ("{0}: {1}", "LongDatePattern", df
.LongDatePattern
);
137 writer
.WriteLine ("{0}: {1}", "LongTimePattern", df
.LongTimePattern
);
138 writer
.WriteLine ("{0}: {1}", "MonthDayPattern", df
.MonthDayPattern
);
139 Dump (writer
, df
.MonthGenitiveNames
, "MonthGenitiveNames");
140 Dump (writer
, df
.MonthNames
, "MonthNames");
141 writer
.WriteLine ("{0}: {1}", "NativeCalendarName", df
.NativeCalendarName
);
142 writer
.WriteLine ("{0}: {1}", "PMDesignator", df
.PMDesignator
);
143 writer
.WriteLine ("{0}: {1}", "ShortDatePattern", df
.ShortDatePattern
);
144 Dump (writer
, df
.ShortestDayNames
, "ShortestDayNames");
145 writer
.WriteLine ("{0}: {1}", "ShortTimePattern", df
.ShortTimePattern
);
146 writer
.WriteLine ("{0}: {1}", "TimeSeparator", df
.TimeSeparator
);
147 writer
.WriteLine ("{0}: {1}", "YearMonthPattern", df
.YearMonthPattern
);
149 var ti
= c
.TextInfoEntry
;
150 writer
.WriteLine ("-- TextInfo --");
151 writer
.WriteLine ("{0}: {1}", "ANSICodePage", ti
.ANSICodePage
);
152 writer
.WriteLine ("{0}: {1}", "EBCDICCodePage", ti
.EBCDICCodePage
);
153 writer
.WriteLine ("{0}: {1}", "IsRightToLeft", ti
.IsRightToLeft
);
154 writer
.WriteLine ("{0}: {1}", "ListSeparator", ti
.ListSeparator
);
155 writer
.WriteLine ("{0}: {1}", "MacCodePage", ti
.MacCodePage
);
156 writer
.WriteLine ("{0}: {1}", "OEMCodePage", ti
.OEMCodePage
);
158 var nf
= c
.NumberFormatEntry
;
159 writer
.WriteLine ("-- NumberFormat --");
160 writer
.WriteLine ("{0}: {1}", "CurrencyDecimalDigits", nf
.CurrencyDecimalDigits
);
161 writer
.WriteLine ("{0}: {1}", "CurrencyDecimalSeparator", nf
.CurrencyDecimalSeparator
);
162 writer
.WriteLine ("{0}: {1}", "CurrencyGroupSeparator", nf
.CurrencyGroupSeparator
);
163 Dump (writer
, nf
.CurrencyGroupSizes
, "CurrencyGroupSizes", true);
164 writer
.WriteLine ("{0}: {1}", "CurrencyNegativePattern", nf
.CurrencyNegativePattern
);
165 writer
.WriteLine ("{0}: {1}", "CurrencyPositivePattern", nf
.CurrencyPositivePattern
);
166 writer
.WriteLine ("{0}: {1}", "CurrencySymbol", nf
.CurrencySymbol
);
167 writer
.WriteLine ("{0}: {1}", "DigitSubstitution", nf
.DigitSubstitution
);
168 writer
.WriteLine ("{0}: {1}", "NaNSymbol", nf
.NaNSymbol
);
169 Dump (writer
, nf
.NativeDigits
, "NativeDigits");
170 writer
.WriteLine ("{0}: {1}", "NegativeInfinitySymbol", nf
.NegativeInfinitySymbol
);
171 writer
.WriteLine ("{0}: {1}", "NegativeSign", nf
.NegativeSign
);
172 writer
.WriteLine ("{0}: {1}", "NumberDecimalDigits", nf
.NumberDecimalDigits
);
173 writer
.WriteLine ("{0}: {1}", "NumberDecimalSeparator", nf
.NumberDecimalSeparator
);
174 writer
.WriteLine ("{0}: {1}", "NumberGroupSeparator", nf
.NumberGroupSeparator
);
175 Dump (writer
, nf
.NumberGroupSizes
, "NumberGroupSizes", true);
176 writer
.WriteLine ("{0}: {1}", "NumberNegativePattern", nf
.NumberNegativePattern
);
177 writer
.WriteLine ("{0}: {1}", "PercentDecimalDigits", nf
.PercentDecimalDigits
);
178 writer
.WriteLine ("{0}: {1}", "PercentDecimalSeparator", nf
.PercentDecimalSeparator
);
179 writer
.WriteLine ("{0}: {1}", "PercentGroupSeparator", nf
.PercentGroupSeparator
);
180 Dump (writer
, nf
.PercentGroupSizes
, "PercentGroupSizes", true);
181 writer
.WriteLine ("{0}: {1}", "PercentNegativePattern", nf
.PercentNegativePattern
);
182 writer
.WriteLine ("{0}: {1}", "PercentPositivePattern", nf
.PercentPositivePattern
);
183 writer
.WriteLine ("{0}: {1}", "PercentSymbol", nf
.PercentSymbol
);
184 writer
.WriteLine ("{0}: {1}", "PerMilleSymbol", nf
.PerMilleSymbol
);
185 writer
.WriteLine ("{0}: {1}", "PositiveInfinitySymbol", nf
.PositiveInfinitySymbol
);
186 writer
.WriteLine ("{0}: {1}", "PositiveSign", nf
.PositiveSign
);
188 if (c
.RegionInfoEntry
!= null) {
189 var ri
= c
.RegionInfoEntry
;
190 writer
.WriteLine ("-- RegionInfo --");
191 writer
.WriteLine ("{0}: {1}", "CurrencyEnglishName", ri
.CurrencyEnglishName
);
192 writer
.WriteLine ("{0}: {1}", "CurrencyNativeName", ri
.CurrencyNativeName
);
193 writer
.WriteLine ("{0}: {1}", "CurrencySymbol", ri
.CurrencySymbol
);
194 writer
.WriteLine ("{0}: {1}", "DisplayName", ri
.DisplayName
);
195 writer
.WriteLine ("{0}: {1}", "EnglishName", ri
.EnglishName
);
196 writer
.WriteLine ("{0}: {1}", "GeoId", ri
.GeoId
);
197 writer
.WriteLine ("{0}: {1}", "IsMetric", ri
.IsMetric
);
198 writer
.WriteLine ("{0}: {1}", "ISOCurrencySymbol", ri
.ISOCurrencySymbol
);
199 writer
.WriteLine ("{0}: {1}", "Name", ri
.Name
);
200 writer
.WriteLine ("{0}: {1}", "NativeName", ri
.NativeName
);
201 writer
.WriteLine ("{0}: {1}", "ThreeLetterISORegionName", ri
.ThreeLetterISORegionName
);
202 writer
.WriteLine ("{0}: {1}", "ThreeLetterWindowsRegionName", ri
.ThreeLetterWindowsRegionName
);
203 writer
.WriteLine ("{0}: {1}", "TwoLetterISORegionName", ri
.TwoLetterISORegionName
);
210 static Type
GetCalendarType (CalendarType ct
)
213 case CalendarType
.Gregorian
:
214 return typeof (GregorianCalendar
);
215 case CalendarType
.HijriCalendar
:
216 return typeof (HijriCalendar
);
217 case CalendarType
.ThaiBuddhist
:
218 return typeof (ThaiBuddhistCalendar
);
219 case CalendarType
.UmAlQuraCalendar
:
220 return typeof (UmAlQuraCalendar
);
222 throw new NotImplementedException ();
226 static void Dump
<T
> (TextWriter tw
, IList
<T
> values
, string name
, bool stopOnNull
= false) where T
: class
231 for (int i
= 0; i
< values
.Count
; ++i
) {
234 if (stopOnNull
&& v
== null)
248 Regex locales_regex
= null;
250 locales_regex
= new Regex (Locales
);
252 cultures
= new List
<CultureInfoEntry
> ();
253 var regions
= new List
<RegionInfoEntry
> ();
256 var supplemental
= GetXmlDocument (Path
.Combine (data_root
, "supplemental", "supplementalData.xml"));
258 // Read currencies info
259 region_currency
= new Dictionary
<string, string> (StringComparer
.OrdinalIgnoreCase
);
260 foreach (XmlNode entry
in supplemental
.SelectNodes ("supplementalData/currencyData/region")) {
261 var child
= entry
.SelectSingleNode ("currency");
262 region_currency
.Add (entry
.Attributes
["iso3166"].Value
, child
.Attributes
["iso4217"].Value
);
266 extra_parent_locales
= new Dictionary
<string, string> (StringComparer
.OrdinalIgnoreCase
);
267 foreach (XmlNode entry
in supplemental
.SelectNodes ("supplementalData/parentLocales/parentLocale")) {
268 var parent
= entry
.Attributes
["parent"].Value
;
270 if (parent
== "root")
273 var locales
= entry
.Attributes
["locales"].Value
;
274 foreach (var locale
in locales
.Split (' '))
275 extra_parent_locales
.Add (locale
, parent
);
278 var lcdids
= GetXmlDocument ("lcids.xml");
279 foreach (XmlNode lcid
in lcdids
.SelectNodes ("lcids/lcid")) {
280 var name
= lcid
.Attributes
["name"].Value
;
282 if (locales_regex
!= null && !locales_regex
.IsMatch (name
))
285 var ci
= new CultureInfoEntry ();
286 ci
.LCID
= lcid
.Attributes
["id"].Value
;
287 ci
.ParentLcid
= lcid
.Attributes
["parent"].Value
;
288 ci
.TwoLetterISOLanguageName
= lcid
.Attributes
["iso2"].Value
;
289 ci
.ThreeLetterISOLanguageName
= lcid
.Attributes
["iso3"].Value
;
290 ci
.ThreeLetterWindowsLanguageName
= lcid
.Attributes
["win"].Value
;
291 ci
.OriginalName
= name
.Replace ('_', '-');
292 ci
.TextInfoEntry
= new TextInfoEntry ();
293 ci
.NumberFormatEntry
= new NumberFormatEntry ();
295 if (!Import (ci
, name
))
301 var doc_english
= GetXmlDocument (Path
.Combine (data_root
, "main", "en.xml"));
304 // Fill all EnglishName values from en.xml language file
306 foreach (var ci
in cultures
) {
307 var el
= doc_english
.SelectSingleNode (string.Format ("ldml/localeDisplayNames/languages/language[@type='{0}']", ci
.Language
));
309 ci
.EnglishName
= el
.InnerText
;
312 if (ci
.Script
!= null) {
313 el
= doc_english
.SelectSingleNode (string.Format ("ldml/localeDisplayNames/scripts/script[@type='{0}']", ci
.Script
));
318 if (ci
.Territory
!= null) {
319 el
= doc_english
.SelectSingleNode (string.Format ("ldml/localeDisplayNames/territories/territory[@type='{0}']", ci
.Territory
));
324 s
= string.Join (", ", s
, el
.InnerText
);
328 switch (ci
.ThreeLetterWindowsLanguageName
) {
338 ci
.EnglishName
= string.Format ("{0} ({1})", ci
.EnglishName
, s
);
340 // Special case legacy chinese
341 if (ci
.OriginalName
== "zh-CHS" || ci
.OriginalName
== "zh-CHT")
342 ci
.EnglishName
+= " Legacy";
344 // Mono is not localized and supports english only, hence the name will always be same
345 ci
.DisplayName
= ci
.EnglishName
;
349 // Fill culture hierarchy for easier data manipulation
351 foreach (var ci
in cultures
) {
352 foreach (var p
in cultures
.Where (l
=> ci
.LCID
== l
.ParentLcid
)) {
357 currency_fractions
= new Dictionary
<string, string> (StringComparer
.OrdinalIgnoreCase
);
358 foreach (XmlNode entry
in supplemental
.SelectNodes ("supplementalData/currencyData/fractions/info")) {
359 currency_fractions
.Add (entry
.Attributes
["iso4217"].Value
, entry
.Attributes
["digits"].Value
);
362 var territory2dayofweek
= new Dictionary
<string, DayOfWeek
> (StringComparer
.OrdinalIgnoreCase
);
363 foreach (XmlNode entry
in supplemental
.SelectNodes ("supplementalData/weekData/firstDay")) {
365 if (entry
.Attributes
["alt"] != null)
369 switch (entry
.Attributes
["day"].Value
) {
371 dow
= DayOfWeek
.Monday
;
374 dow
= DayOfWeek
.Friday
;
377 dow
= DayOfWeek
.Saturday
;
380 dow
= DayOfWeek
.Sunday
;
383 throw new NotImplementedException ();
386 var territories
= entry
.Attributes
["territories"].Value
.Split ();
387 foreach (var t
in territories
) {
388 territory2dayofweek
.Add (t
, dow
);
392 var territory2wr
= new Dictionary
<string, CalendarWeekRule
> (StringComparer
.OrdinalIgnoreCase
);
393 foreach (XmlNode entry
in supplemental
.SelectNodes ("supplementalData/weekData/minDays")) {
394 CalendarWeekRule rule
;
396 switch (entry
.Attributes
["count"].InnerText
) {
398 rule
= CalendarWeekRule
.FirstDay
;
401 rule
= CalendarWeekRule
.FirstFourDayWeek
;
404 throw new NotImplementedException ();
407 var territories
= entry
.Attributes
["territories"].InnerText
.Split ();
408 foreach (var t
in territories
)
409 territory2wr
[t
] = rule
;
413 // Fill all territory speficic data where territory is available
415 var non_metric
= new HashSet
<string> ();
416 foreach (XmlNode entry
in supplemental
.SelectNodes ("supplementalData/measurementData/measurementSystem[@type='US']")) {
417 var territories
= entry
.Attributes
["territories"].InnerText
.Split ();
418 foreach (var t
in territories
)
422 foreach (var ci
in cultures
) {
423 if (ci
.Territory
== null)
427 if (territory2dayofweek
.TryGetValue (ci
.Territory
, out value)) {
428 ci
.DateTimeFormatEntry
.FirstDayOfWeek
= (int) value;
431 CalendarWeekRule rule
;
432 if (territory2wr
.TryGetValue (ci
.Territory
, out rule
)) {
433 ci
.DateTimeFormatEntry
.CalendarWeekRule
= (int) rule
;
436 RegionInfoEntry region
= regions
.Where (l
=> l
.Name
== ci
.Territory
).FirstOrDefault ();
437 if (region
== null) {
438 region
= new RegionInfoEntry () {
439 CurrencySymbol
= ci
.NumberFormatEntry
.CurrencySymbol
,
440 EnglishName
= ci
.EnglishName
,
441 NativeName
= ci
.NativeTerritoryName
,
443 TwoLetterISORegionName
= ci
.Territory
,
444 CurrencyNativeName
= ci
.NativeCurrencyName
447 var tc
= supplemental
.SelectSingleNode (string.Format ("supplementalData/codeMappings/territoryCodes[@type='{0}']", ci
.Territory
));
448 region
.ThreeLetterISORegionName
= tc
.Attributes
["alpha3"].Value
;
449 region
.ThreeLetterWindowsRegionName
= region
.ThreeLetterISORegionName
;
451 var el
= doc_english
.SelectSingleNode (string.Format ("ldml/localeDisplayNames/territories/territory[@type='{0}']", ci
.Territory
));
452 region
.EnglishName
= el
.InnerText
;
453 region
.DisplayName
= region
.EnglishName
;
455 region
.ISOCurrencySymbol
= region_currency
[ci
.Territory
];
457 el
= doc_english
.SelectSingleNode (string.Format ("ldml/numbers/currencies/currency[@type='{0}']/displayName", region
.ISOCurrencySymbol
));
458 region
.CurrencyEnglishName
= el
.InnerText
;
460 if (non_metric
.Contains (ci
.Territory
))
461 region
.IsMetric
= false;
463 var lcdid_value
= int.Parse (ci
.LCID
.Substring (2), NumberStyles
.HexNumber
);
464 Patterns
.FillValues (lcdid_value
, region
);
465 regions
.Add (region
);
468 string fraction_value
;
469 if (currency_fractions
.TryGetValue (region
.ISOCurrencySymbol
, out fraction_value
)) {
470 ci
.NumberFormatEntry
.CurrencyDecimalDigits
= fraction_value
;
473 ci
.RegionInfoEntry
= region
;
477 // Fill neutral cultures territory data
479 foreach (var ci
in cultures
) {
480 var dtf
= ci
.DateTimeFormatEntry
;
481 if (dtf
.FirstDayOfWeek
== null) {
484 dtf
.FirstDayOfWeek
= (int) DayOfWeek
.Saturday
;
489 dtf
.FirstDayOfWeek
= (int) DayOfWeek
.Sunday
;
496 dtf
.FirstDayOfWeek
= (int) DayOfWeek
.Monday
;
499 List
<int?> all_fdow
= new List
<int?> ();
500 GetAllChildrenValues (ci
, all_fdow
, l
=> l
.DateTimeFormatEntry
.FirstDayOfWeek
);
501 var children
= all_fdow
.Where (l
=> l
!= null).Distinct ().ToList ();
503 if (children
.Count
== 1) {
504 dtf
.FirstDayOfWeek
= children
[0];
505 } else if (children
.Count
== 0) {
506 if (!ci
.HasMissingLocale
)
507 Console
.WriteLine ("No week data for `{0}'", ci
.Name
);
510 dtf
.FirstDayOfWeek
= (int) DayOfWeek
.Sunday
;
512 // .NET has weird concept of territory data available for neutral cultures (e.g. en, es, pt)
513 // We have to manually disambiguate the correct entry (which is artofficial anyway)
514 throw new ApplicationException (string.Format ("Ambiguous week data for `{0}'", ci
.Name
));
521 if (dtf
.CalendarWeekRule
== null) {
530 dtf
.CalendarWeekRule
= (int) CalendarWeekRule
.FirstDay
;
533 List
<int?> all_cwr
= new List
<int?> ();
534 GetAllChildrenValues (ci
, all_cwr
, l
=> l
.DateTimeFormatEntry
.CalendarWeekRule
);
535 var children
= all_cwr
.Where (l
=> l
!= null).Distinct ().ToList ();
537 if (children
.Count
== 1) {
538 dtf
.CalendarWeekRule
= children
[0];
539 } else if (children
.Count
== 0) {
540 if (!ci
.HasMissingLocale
)
541 Console
.WriteLine ("No calendar week data for `{0}'", ci
.Name
);
544 // Default to FirstDay
545 dtf
.CalendarWeekRule
= (int) CalendarWeekRule
.FirstDay
;
547 // .NET has weird concept of territory data available for neutral cultures (e.g. en, es, pt)
548 // We have to manually disambiguate the correct entry (which is artofficial anyway)
549 throw new ApplicationException (string.Format ("Ambiguous calendar data for `{0}'", ci
.Name
));
556 var nfe
= ci
.NumberFormatEntry
;
557 if (nfe
.CurrencySymbol
== null) {
560 nfe
.CurrencySymbol
= "ر.س.";
563 nfe
.CurrencySymbol
= "$";
566 nfe
.CurrencySymbol
= "KM";
573 nfe
.CurrencySymbol
= "€";
576 nfe
.CurrencySymbol
= "kn";
579 nfe
.CurrencySymbol
= "R$";
582 nfe
.CurrencySymbol
= "kr";
585 nfe
.CurrencySymbol
= "RM";
588 nfe
.CurrencySymbol
= "টা";
591 nfe
.CurrencySymbol
= "Дин.";
595 nfe
.CurrencySymbol
= "Din.";
599 nfe
.CurrencySymbol
= "¥";
602 nfe
.CurrencySymbol
= "HK$";
606 var all_currencies
= new List
<string> ();
607 GetAllChildrenValues (ci
, all_currencies
, l
=> l
.NumberFormatEntry
.CurrencySymbol
);
608 var children
= all_currencies
.Where (l
=> l
!= null).Distinct ().ToList ();
610 if (children
.Count
== 1) {
611 nfe
.CurrencySymbol
= children
[0];
612 } else if (children
.Count
== 0) {
613 if (!ci
.HasMissingLocale
)
614 Console
.WriteLine ("No currency data for `{0}'", ci
.Name
);
618 // .NET has weird concept of territory data available for neutral cultures (e.g. en, es, pt)
619 // We have to manually disambiguate the correct entry (which is artofficial anyway)
620 throw new ApplicationException (string.Format ("Ambiguous currency data for `{0}'. Possible values '{1}'", ci
.Name
, string.Join (", ", children
)));
627 if (nfe
.CurrencyDecimalDigits
== null) {
628 var all_digits
= new List
<string> ();
629 GetAllChildrenValues (ci
, all_digits
, l
=> l
.NumberFormatEntry
.CurrencyDecimalDigits
);
630 var children
= all_digits
.Where (l
=> l
!= null).Distinct ().ToList ();
632 if (children
.Count
== 1) {
633 nfe
.CurrencyDecimalDigits
= children
[0];
634 } else if (children
.Count
== 0) {
635 if (!ci
.HasMissingLocale
)
636 Console
.WriteLine ("No currency decimal digits data for `{0}'", ci
.Name
);
638 nfe
.CurrencyDecimalDigits
= "2";
639 } else if (ci
.IsNeutral
) {
640 nfe
.CurrencyDecimalDigits
= "2";
642 // .NET has weird concept of territory data available for neutral cultures (e.g. en, es, pt)
643 // We have to manually disambiguate the correct entry (which is artofficial anyway)
644 throw new ApplicationException (string.Format ("Ambiguous currency decimal digits data for `{0}'", ci
.Name
));
652 regions
.Sort (new RegionComparer ());
653 for (int i
= 0; i
< regions
.Count
; ++i
)
654 regions
[i
].Index
= i
;
657 * Dump each table individually. Using StringBuilders
658 * because it is easier to debug, should switch to just
659 * writing to streams eventually.
661 using (StreamWriter writer
= new StreamWriter (HeaderFileName
, false, new UTF8Encoding (false, true))) {
662 writer
.NewLine
= "\n";
664 writer
.WriteLine ("/* This is a generated file. Do not edit. See tools/locale-builder. */");
665 writer
.WriteLine ("#ifndef MONO_METADATA_CULTURE_INFO_TABLES");
666 writer
.WriteLine ("#define MONO_METADATA_CULTURE_INFO_TABLES 1");
667 writer
.WriteLine ("\n");
669 writer
.WriteLine ("#define NUM_CULTURE_ENTRIES {0}", cultures
.Count
);
670 writer
.WriteLine ("#define NUM_REGION_ENTRIES {0}", regions
.Count
);
672 writer
.WriteLine ("\n");
674 // Sort the cultures by lcid
675 cultures
.Sort (new LcidComparer ());
677 StringBuilder builder
= new StringBuilder ();
679 int count
= cultures
.Count
;
680 for (int i
= 0; i
< count
; i
++) {
681 CultureInfoEntry ci
= cultures
[i
];
682 if (ci
.DateTimeFormatEntry
== null)
684 ci
.DateTimeFormatEntry
.AppendTableRow (builder
);
685 ci
.DateTimeFormatEntry
.Row
= row
++;
687 builder
.Append (',');
688 builder
.Append ('\n');
691 writer
.WriteLine ("static const DateTimeFormatEntry datetime_format_entries [] = {");
692 writer
.Write (builder
);
693 writer
.WriteLine ("};\n\n");
695 builder
= new StringBuilder ();
697 for (int i
= 0; i
< count
; i
++) {
698 CultureInfoEntry ci
= cultures
[i
];
699 if (ci
.NumberFormatEntry
== null)
701 ci
.NumberFormatEntry
.AppendTableRow (builder
);
702 ci
.NumberFormatEntry
.Row
= row
++;
704 builder
.Append (',');
705 builder
.Append ('\n');
708 writer
.WriteLine ("static const NumberFormatEntry number_format_entries [] = {");
709 writer
.Write (builder
);
710 writer
.WriteLine ("};\n\n");
712 builder
= new StringBuilder ();
714 for (int i
= 0; i
< count
; i
++) {
715 CultureInfoEntry ci
= cultures
[i
];
716 ci
.AppendTableRow (builder
);
719 builder
.Append (',');
720 builder
.Append ('\n');
723 writer
.WriteLine ("static const CultureInfoEntry culture_entries [] = {");
724 writer
.Write (builder
);
725 writer
.WriteLine ("};\n\n");
727 cultures
.Sort (new ExportNameComparer ()); // Sort based on name
728 builder
= new StringBuilder ();
729 for (int i
= 0; i
< count
; i
++) {
730 CultureInfoEntry ci
= cultures
[i
];
731 var name
= ci
.GetExportName ().ToLowerInvariant ();
732 builder
.Append ("\t{" + Entry
.EncodeStringIdx (name
) + ", ");
733 builder
.Append (ci
.Row
+ "}");
735 builder
.Append (',');
737 builder
.AppendFormat ("\t /* {0} */", name
);
738 builder
.Append ('\n');
741 writer
.WriteLine ("static const CultureInfoNameEntry culture_name_entries [] = {");
742 writer
.Write (builder
);
743 writer
.WriteLine ("};\n\n");
745 builder
= new StringBuilder ();
747 foreach (RegionInfoEntry r
in regions
) {
748 r
.AppendTableRow (builder
);
749 if (++rcount
!= regions
.Count
)
750 builder
.Append (',');
752 builder
.Append ('\n');
754 writer
.WriteLine ("static const RegionInfoEntry region_entries [] = {");
755 writer
.Write (builder
);
756 writer
.WriteLine ("};\n\n");
758 builder
= new StringBuilder ();
760 foreach (RegionInfoEntry ri
in regions
) {
761 builder
.Append ("\t{" + Entry
.EncodeStringIdx (ri
.TwoLetterISORegionName
) + ", ");
762 builder
.Append (ri
.Index
+ "}");
763 if (++rcount
!= regions
.Count
)
764 builder
.Append (',');
766 builder
.AppendFormat ("\t /* {0} */", ri
.TwoLetterISORegionName
);
767 builder
.Append ('\n');
770 writer
.WriteLine ("static const RegionInfoNameEntry region_name_entries [] = {");
771 writer
.Write (builder
);
772 writer
.WriteLine ("};\n\n");
774 writer
.WriteLine ("static const char locale_strings [] = {");
775 writer
.Write (Entry
.GetStrings ());
776 writer
.WriteLine ("};\n\n");
778 writer
.WriteLine ("#endif\n");
782 static void GetAllChildrenValues
<T
> (CultureInfoEntry entry
, List
<T
> values
, Func
<CultureInfoEntry
, T
> selector
)
784 foreach (var e
in entry
.Children
) {
788 values
.Add (selector (e
));
790 foreach (var e2
in e
.Children
) {
791 GetAllChildrenValues (e2
, values
, selector
);
796 static XmlDocument
GetXmlDocument (string path
)
798 var doc
= new XmlDocument ();
799 doc
.Load (new XmlTextReader (path
) { /*DtdProcessing = DtdProcessing.Ignore*/ }
);
803 bool Import (CultureInfoEntry data
, string locale
)
806 var sep
= locale
.Split ('_');
807 data
.Language
= sep
[0];
809 // CLDR strictly follow ISO names, .NET does not
810 // Replace names where non-iso2 is used, e.g. Norway
811 if (data
.Language
!= data
.TwoLetterISOLanguageName
) {
812 locale
= data
.TwoLetterISOLanguageName
;
813 if (sep
.Length
> 1) {
814 locale
+= string.Join ("_", sep
.Skip (1));
818 // Convert broken Chinese names to correct one
827 locale
= "zh_Hans_CN";
830 locale
= "zh_Hant_HK";
833 locale
= "zh_Hans_SG";
836 locale
= "zh_Hant_TW";
839 locale
= "zh_Hant_MO";
843 sep
= locale
.Split ('_');
845 string full_name
= Path
.Combine (data_root
, "main", locale
+ ".xml");
846 if (!File
.Exists (full_name
)) {
847 Console
.WriteLine ("Missing locale file for `{0}'", locale
);
849 // We could fill default values but that's not as simple as it seems. For instance for non-neutral
850 // cultures the next part could be territory or not.
853 XmlDocument doc
= null;
856 * Locale generation is done in several steps, first we
857 * read the root file which is the base invariant data
858 * then the supplemental root data,
859 * then the language file, the supplemental languages
860 * file then the locale file, then the supplemental
861 * locale file. Values in each descending file can
862 * overwrite previous values.
864 foreach (var part
in sep
) {
872 if (extra_parent_locales
.TryGetValue (fname
, out extra
)) {
873 xml
= GetXmlDocument (Path
.Combine (data_root
, "main", extra
+ ".xml"));
880 xml
= GetXmlDocument (Path
.Combine (data_root
, "main", fname
+ ".xml"));
888 // Extract localized locale name from language xml file. Have to do it after both language and territory are read
890 var el
= doc
.SelectSingleNode (string.Format ("ldml/localeDisplayNames/languages/language[@type='{0}']", data
.Language
));
892 data
.NativeName
= el
.InnerText
;
894 if (data
.Territory
!= null) {
895 el
= doc
.SelectSingleNode (string.Format ("ldml/localeDisplayNames/territories/territory[@type='{0}']", data
.Territory
));
897 // TODO: Should read <localePattern>
898 data
.NativeName
= string.Format ("{0} ({1})", data
.NativeName
, el
.InnerText
);
899 data
.NativeTerritoryName
= el
.InnerText
;
903 // We have territory now we have to run the process again to extract currency symbol
904 if (region_currency
.TryGetValue (data
.Territory
, out currency
)) {
907 var xml
= GetXmlDocument (Path
.Combine (data_root
, "main", "root.xml"));
908 el
= xml
.SelectSingleNode (string.Format ("ldml/numbers/currencies/currency[@type='{0}']/symbol", currency
));
910 data
.NumberFormatEntry
.CurrencySymbol
= el
.InnerText
;
912 foreach (var part
in sep
) {
918 xml
= GetXmlDocument (Path
.Combine (data_root
, "main", fname
+ ".xml"));
919 el
= xml
.SelectSingleNode (string.Format ("ldml/numbers/currencies/currency[@type='{0}']/symbol", currency
));
921 data
.NumberFormatEntry
.CurrencySymbol
= el
.InnerText
;
923 el
= xml
.SelectSingleNode (string.Format ("ldml/numbers/currencies/currency[@type='{0}']/displayName", currency
));
925 data
.NativeCurrencyName
= el
.InnerText
;
931 // It looks like it never changes
932 data
.DateTimeFormatEntry
.TimeSeparator
= ":";
934 // TODO: Don't have input data available but most values are 2 with few exceptions for 1 and 3
935 // We don't add 3 as it's for some arabic states only
936 switch (data
.ThreeLetterISOLanguageName
) {
938 data
.NumberFormatEntry
.NumberDecimalDigits
=
939 data
.NumberFormatEntry
.PercentDecimalDigits
= 1;
942 data
.NumberFormatEntry
.NumberDecimalDigits
=
943 data
.NumberFormatEntry
.PercentDecimalDigits
= 2;
947 // TODO: For now we capture only native name for default calendar
948 data
.NativeCalendarNames
[((int) data
.CalendarType
& 0xFF) - 1] = data
.DateTimeFormatEntry
.NativeCalendarName
;
950 var lcdid_value
= int.Parse (data
.LCID
.Substring (2), NumberStyles
.HexNumber
);
951 Patterns
.FillValues (lcdid_value
, data
);
956 void Import (XmlDocument doc
, CultureInfoEntry ci
)
962 // Extract script & teritory
964 el
= doc
.SelectSingleNode ("ldml/identity/script");
966 ci
.Script
= el
.Attributes
["type"].Value
;
968 el
= doc
.SelectSingleNode ("ldml/identity/territory");
970 ci
.Territory
= el
.Attributes
["type"].Value
;
972 var df
= ci
.DateTimeFormatEntry
;
975 // Default calendar is for now always "gregorian"
977 case "th": case "th-TH":
978 calendar
= "buddhist";
979 ci
.CalendarType
= CalendarType
.ThaiBuddhist
; // typeof (ThaiBuddhistCalendar);
981 case "ar": case "ar-SA":
982 calendar
= "islamic";
983 ci
.CalendarType
= CalendarType
.UmAlQuraCalendar
; // typeof (UmAlQuraCalendar);
985 case "ps": case "ps-AF": case "prs": case "prs-AF": case "dv": case "dv-MV":
986 calendar
= "persian";
987 ci
.CalendarType
= CalendarType
.HijriCalendar
; // typeof (HijriCalendar);
990 calendar
= "gregorian";
991 ci
.CalendarType
= CalendarType
.Gregorian
; // typeof (GregorianCalendar);
992 ci
.GregorianCalendarType
= GregorianCalendarTypes
.Localized
;
996 var node
= doc
.SelectSingleNode (string.Format ("ldml/dates/calendars/calendar[@type='{0}']", calendar
));
998 el
= doc
.SelectSingleNode (string.Format ("ldml/localeDisplayNames/types/type[@type='{0}']", calendar
));
1000 df
.NativeCalendarName
= el
.InnerText
;
1003 // Apply global rule first <alias source="locale" path="../../monthContext[@type='format']/monthWidth[@type='wide']"/>
1004 nodes
= node
.SelectNodes ("months/monthContext[@type='format']/monthWidth[@type='wide']/month");
1005 ProcessAllNodes (nodes
, df
.MonthNames
, AddOrReplaceValue
);
1006 nodes
= node
.SelectNodes ("months/monthContext[@type='stand-alone']/monthWidth[@type='wide']/month");
1007 ProcessAllNodes (nodes
, df
.MonthNames
, AddOrReplaceValue
);
1009 // Apply global rule first <alias source="locale" path="../../monthContext[@type='format']/monthWidth[@type='abbreviated']"/>
1010 if (ci
.Name
== "ja" || ci
.Name
== "ja-JP") {
1011 // Use common number style
1013 nodes
= node
.SelectNodes ("months/monthContext[@type='format']/monthWidth[@type='abbreviated']/month");
1014 ProcessAllNodes (nodes
, df
.AbbreviatedMonthNames
, AddOrReplaceValue
);
1015 nodes
= node
.SelectNodes ("months/monthContext[@type='stand-alone']/monthWidth[@type='abbreviated']/month");
1016 ProcessAllNodes (nodes
, df
.AbbreviatedMonthNames
, AddOrReplaceValue
);
1019 nodes
= node
.SelectNodes ("months/monthContext[@type='format']/monthWidth[@type='wide']/month");
1020 if (nodes
!= null) {
1021 ProcessAllNodes (nodes
, df
.MonthGenitiveNames
, AddOrReplaceValue
);
1024 // All values seem to match
1025 Array
.Copy (df
.AbbreviatedMonthNames
, df
.AbbreviatedMonthGenitiveNames
, df
.AbbreviatedMonthNames
.Length
);
1027 nodes
= node
.SelectNodes ("days/dayContext[@type='format']/dayWidth[@type='wide']/day");
1028 ProcessAllNodes (nodes
, df
.DayNames
, AddOrReplaceDayValue
);
1030 // Apply global rule first <alias source="locale" path="../../dayContext[@type='format']/dayWidth[@type='abbreviated']"/>
1031 nodes
= node
.SelectNodes ("days/dayContext[@type='format']/dayWidth[@type='abbreviated']/day");
1032 ProcessAllNodes (nodes
, df
.AbbreviatedDayNames
, AddOrReplaceDayValue
);
1033 nodes
= node
.SelectNodes ("days/dayContext[@type='stand-alone']/dayWidth[@type='abbreviated']/day");
1034 ProcessAllNodes (nodes
, df
.AbbreviatedDayNames
, AddOrReplaceDayValue
);
1036 // TODO: This is not really ShortestDayNames as .NET uses it
1037 // Apply global rules first <alias source="locale" path="../../dayContext[@type='stand-alone']/dayWidth[@type='narrow']"/>
1038 nodes
= node
.SelectNodes ("days/dayContext[@type='format']/dayWidth[@type='narrow']/day");
1039 ProcessAllNodes (nodes
, df
.ShortestDayNames
, AddOrReplaceDayValue
);
1040 nodes
= node
.SelectNodes ("days/dayContext[@type='stand-alone']/dayWidth[@type='narrow']/day");
1041 ProcessAllNodes (nodes
, df
.ShortestDayNames
, AddOrReplaceDayValue
);
1043 Cannot really be used it's too different to .NET and most app rely on it
1045 el = node.SelectSingleNode ("dateFormats/dateFormatLength[@type='full']/dateFormat/pattern");
1047 df.LongDatePattern = ConvertDatePatternFormat (el.InnerText);
1049 // Medium is our short
1050 el = node.SelectSingleNode ("dateFormats/dateFormatLength[@type='medium']/dateFormat/pattern");
1052 df.ShortDatePattern = ConvertDatePatternFormat (el.InnerText);
1054 // Medium is our Long
1055 el = node.SelectSingleNode ("timeFormats/timeFormatLength[@type='medium']/timeFormat/pattern");
1057 df.LongTimePattern = ConvertTimePatternFormat (el.InnerText);
1059 el = node.SelectSingleNode ("timeFormats/timeFormatLength[@type='short']/timeFormat/pattern");
1061 df.ShortTimePattern = ConvertTimePatternFormat (el.InnerText);
1063 el = node.SelectSingleNode ("dateTimeFormats/availableFormats/dateFormatItem[@id='yyyyMMMM']");
1065 df.YearMonthPattern = ConvertDatePatternFormat (el.InnerText);
1067 el = node.SelectSingleNode ("dateTimeFormats/availableFormats/dateFormatItem[@id='MMMMdd']");
1069 df.MonthDayPattern = ConvertDatePatternFormat (el.InnerText);
1071 el
= node
.SelectSingleNode ("dayPeriods/dayPeriodContext/dayPeriodWidth[@type='abbreviated']/dayPeriod[@type='am']");
1073 // Apply global rule first <alias source="locale" path="../dayPeriodWidth[@type='wide']"/>
1074 el
= node
.SelectSingleNode ("dayPeriods/dayPeriodContext/dayPeriodWidth[@type='wide']/dayPeriod[@type='am']");
1078 df
.AMDesignator
= "AM";
1082 df
.AMDesignator
= el
.InnerText
;
1086 el
= node
.SelectSingleNode ("dayPeriods/dayPeriodContext/dayPeriodWidth[@type='abbreviated']/dayPeriod[@type='pm']");
1088 // Apply global rule first <alias source="locale" path="../dayPeriodWidth[@type='wide']"/>
1089 el
= node
.SelectSingleNode ("dayPeriods/dayPeriodContext/dayPeriodWidth[@type='wide']/dayPeriod[@type='pm']");
1093 df
.PMDesignator
= "PM";
1097 df
.PMDesignator
= el
.InnerText
;
1102 var ni
= ci
.NumberFormatEntry
;
1104 node
= doc
.SelectSingleNode ("ldml/numbers/symbols");
1106 el
= node
.SelectSingleNode ("decimal");
1108 ni
.NumberDecimalSeparator
=
1109 ni
.PercentDecimalSeparator
= el
.InnerText
;
1112 el
= node
.SelectSingleNode ("plusSign");
1114 ni
.PositiveSign
= el
.InnerText
;
1116 el
= node
.SelectSingleNode ("minusSign");
1118 // CLDR uses unicode negative sign for some culture (e.g sv, is, lt, don't kwnow why) but .net always
1119 // uses simple - sign
1120 if (el
.InnerText
== "\u2212") {
1121 ni
.NegativeSign
= "-";
1122 } else if (el
.InnerText
== "\u200F\u002D") {
1123 // Remove any right-to-left mark characters
1124 ni
.NegativeSign
= "-";
1126 ni
.NegativeSign
= el
.InnerText
;
1129 el
= node
.SelectSingleNode ("infinity");
1131 // We cannot use the value from CLDR because many broken
1132 // .NET serializers (e.g. JSON) use text value of NegativeInfinity
1133 // and different value would break interoperability with .NET
1134 var inf
= GetInfinitySymbol (ci
);
1136 ni
.InfinitySymbol
= inf
;
1137 else if (el
!= null && el
.InnerText
!= "∞") {
1138 ni
.InfinitySymbol
= el
.InnerText
;
1141 el
= node
.SelectSingleNode ("perMille");
1143 ni
.PerMilleSymbol
= el
.InnerText
;
1145 el
= node
.SelectSingleNode ("nan");
1147 ni
.NaNSymbol
= el
.InnerText
;
1149 el
= node
.SelectSingleNode ("percentSign");
1151 ni
.PercentSymbol
= el
.InnerText
;
1155 string value = null;
1157 // .net has incorrect separators for some countries and we want to be compatible
1160 // es-ES does not have group separator but .net has '.'
1165 el
= node
.SelectSingleNode ("group");
1167 value = el
.InnerText
;
1174 if (value != null) {
1175 ni
.NumberGroupSeparator
=
1176 ni
.PercentGroupSeparator
=
1177 ni
.CurrencyGroupSeparator
= value;
1181 string GetInfinitySymbol (CultureInfoEntry ci
)
1184 switch (ci
.TwoLetterISOLanguageName
) {
1189 return "+nekonečno";
1191 return "+unendlich";
1206 return "+nieskończoność";
1209 return "бесконечность";
1211 return "neskončnost";
1225 static string ConvertDatePatternFormat (string format
)
1228 // LDMR uses different characters for some fields
1229 // http://unicode.org/reports/tr35/#Date_Format_Patterns
1231 format
= format
.Replace ("EEEE", "dddd"); // The full name of the day of the week
1232 format
= format
.Replace ("LLLL", "MMMM"); // The full month name
1234 if (format
.EndsWith (" y", StringComparison
.Ordinal
))
1240 static string ConvertTimePatternFormat (string format
)
1242 format
= format
.Replace ("a", "tt"); // AM or PM
1246 static void ProcessAllNodes (XmlNodeList list
, IList
<string> values
, Action
<IList
<string>, string, string> convertor
)
1248 foreach (XmlNode entry
in list
) {
1249 var index
= entry
.Attributes
["type"].Value
;
1250 var value = entry
.InnerText
;
1251 convertor (values
, index
, value);
1255 // All text indexes are 1-based
1256 static void AddOrReplaceValue (IList
<string> list
, string oneBasedIndex
, string value)
1258 int index
= int.Parse (oneBasedIndex
);
1259 AddOrReplaceValue (list
, index
- 1, value);
1262 static readonly string[] day_types
= new string[] { "sun", "mon", "tue", "wed", "thu", "fri", "sat" }
;
1264 static void AddOrReplaceDayValue (IList
<string> list
, string dayType
, string value)
1266 int index
= Array
.IndexOf (day_types
, dayType
);
1267 AddOrReplaceValue (list
, index
, value);
1270 static void AddOrReplaceValue (IList
<string> list
, int index
, string value)
1272 if (list
.Count
<= index
)
1273 ((List
<string>) list
).AddRange (new string[index
- list
.Count
+ 1]);
1275 list
[index
] = value;
1278 sealed class LcidComparer
: IComparer
<CultureInfoEntry
>
1280 public int Compare (CultureInfoEntry x
, CultureInfoEntry y
)
1282 return x
.LCID
.CompareTo (y
.LCID
);
1286 sealed class ExportNameComparer
: IComparer
<CultureInfoEntry
>
1288 public int Compare (CultureInfoEntry x
, CultureInfoEntry y
)
1290 return String
.Compare (x
.GetExportName (), y
.GetExportName (), StringComparison
.OrdinalIgnoreCase
);
1294 class RegionComparer
: IComparer
<RegionInfoEntry
>
1296 public int Compare (RegionInfoEntry x
, RegionInfoEntry y
)
1298 return x
.TwoLetterISORegionName
.CompareTo (y
.TwoLetterISORegionName
);