/
Tx.cs
4830 lines (4421 loc) · 162 KB
/
Tx.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// TxLib – Tx Translation & Localisation for .NET and WPF
// © Yves Goergen, Made in Germany
// Website: http://unclassified.software/source/txtranslation
//
// This library is free software: you can redistribute it and/or modify it under the terms of
// the GNU Lesser General Public License as published by the Free Software Foundation, version 3.
//
// This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with this
// library. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
namespace Unclassified.TxLib
{
/// <summary>
/// Provides translation and localisation methods.
/// </summary>
public static class Tx
{
#region Constants
/// <summary>
/// Environment variable name that specifies the log file name. If the environment variable
/// is set but empty, the log messages are sent to Trace. The environment variable is only
/// evaluated when the process starts.
/// </summary>
private const string LogFileEnvironmentVariableName = "TX_LOG_FILE";
/// <summary>
/// Environment variable name that indicates whether the usage of text keys is tracked and
/// unused text keys from the primary culture are logged before the process ends. To enable
/// this option, set the environment variable value to "1". This will only work if a log
/// file is specified (not Trace) and a primary culture has been set. If this option is
/// enabled, placeholder names provided by the application but unused by the translated
/// text will also be logged to the current log target. The environment variable is only
/// evaluated when the process starts.
/// </summary>
private const string LogUnusedEnvironmentVariableName = "TX_LOG_UNUSED";
private const int WriteLockTimeout = 10000;
private const int ReadLockTimeout = 1000;
private const int ReloadChangesDelay = 2000;
/// <summary>
/// Defines text keys that are used by various format methods of the Tx class.
/// </summary>
public static class SystemKeys
{
/// <summary>The system text key for a colon.</summary>
public const string Colon = "Tx:colon";
/// <summary>The system text key for an opening parenthesis.</summary>
public const string ParenthesisBegin = "Tx:parenthesis begin";
/// <summary>The system text key for a closing parenthesis.</summary>
public const string ParenthesisEnd = "Tx:parenthesis end";
/// <summary>The system text key for an opening quotation mark.</summary>
public const string QuoteBegin = "Tx:quote begin";
/// <summary>The system text key for a closing quotation mark.</summary>
public const string QuoteEnd = "Tx:quote end";
/// <summary>The system text key for an opening nested quotation mark.</summary>
public const string QuoteNestedBegin = "Tx:quote nested begin";
/// <summary>The system text key for a closing nested quotation mark.</summary>
public const string QuoteNestedEnd = "Tx:quote nested end";
/// <summary>The system text key for the unit name of a byte.</summary>
public const string ByteUnit = "Tx:byte unit";
/// <summary>The system text key for a negative number indicator.</summary>
public const string NumberNegative = "Tx:number.negative";
/// <summary>The system text key for a number decimal separator.</summary>
public const string NumberDecimalSeparator = "Tx:number.decimal separator";
/// <summary>The system text key for a number thousands group separator.</summary>
public const string NumberGroupSeparator = "Tx:number.group separator";
/// <summary>The system text key for the threshold value from which on to use the thousands group separator.</summary>
public const string NumberGroupSeparatorThreshold = "Tx:number.group separator threshold";
/// <summary>The system text key for a separator between a number and a unit name.</summary>
public const string NumberUnitSeparator = "Tx:number.unit separator";
/// <summary>The system text key for an ordinal number suffix.</summary>
public const string NumberOrdinal = "Tx:number.ordinal";
/// <summary>The system text key for a female ordinal number suffix.</summary>
public const string NumberOrdinalFeminin = "Tx:number.ordinal f";
/// <summary>The system text key for the date format containing a year only.</summary>
public const string DateYear = "Tx:date.year";
/// <summary>The system text key for the date format containing a year and month.</summary>
public const string DateYearMonth = "Tx:date.year month";
/// <summary>The system text key for the date format containing a year and month in tabular form (fixed-length).</summary>
public const string DateYearMonthTab = "Tx:date.year month.tab";
/// <summary>The system text key for the date format containing a year and abbreviated month.</summary>
public const string DateYearMonthAbbr = "Tx:date.year month.abbr";
/// <summary>The system text key for the date format containing a year and long month.</summary>
public const string DateYearMonthLong = "Tx:date.year month.long";
/// <summary>The system text key for the date format containing a year, month and day.</summary>
public const string DateYearMonthDay = "Tx:date.year month day";
/// <summary>The system text key for the date format containing a year, month and day in tabular form (fixed-length).</summary>
public const string DateYearMonthDayTab = "Tx:date.year month day.tab";
/// <summary>The system text key for the date format containing a year, abbreviated month and day.</summary>
public const string DateYearMonthDayAbbr = "Tx:date.year month day.abbr";
/// <summary>The system text key for the date format containing a year, long month and day.</summary>
public const string DateYearMonthDayLong = "Tx:date.year month day.long";
/// <summary>The system text key for the date format containing a month only.</summary>
public const string DateMonth = "Tx:date.month";
/// <summary>The system text key for the date format containing a month only in tabular form (fixed-length).</summary>
public const string DateMonthTab = "Tx:date.month.tab";
/// <summary>The system text key for the date format containing an abbreviated month only.</summary>
public const string DateMonthAbbr = "Tx:date.month.abbr";
/// <summary>The system text key for the date format containing a long month only.</summary>
public const string DateMonthLong = "Tx:date.month.long";
/// <summary>The system text key for the date format containing a month and day.</summary>
public const string DateMonthDay = "Tx:date.month day";
/// <summary>The system text key for the date format containing a month and day in tabular form (fixed-length).</summary>
public const string DateMonthDayTab = "Tx:date.month day.tab";
/// <summary>The system text key for the date format containing an abbreviated month and day.</summary>
public const string DateMonthDayAbbr = "Tx:date.month day.abbr";
/// <summary>The system text key for the date format containing a long month and day.</summary>
public const string DateMonthDayLong = "Tx:date.month day.long";
/// <summary>The system text key for the date format containing a day only.</summary>
public const string DateDay = "Tx:date.day";
/// <summary>The system text key for the date format containing a day only in tabular form (fixed-length).</summary>
public const string DateDayTab = "Tx:date.day.tab";
/// <summary>The system text key for the date format containing a year and quarter.</summary>
public const string DateYearQuarter = "Tx:date.year quarter";
/// <summary>The system text key for the date format containing a quarter only.</summary>
public const string DateQuarter = "Tx:date.quarter";
/// <summary>The system text key for the date format containing a day of week with date.</summary>
public const string DateDowWithDate = "Tx:date.dow with date";
/// <summary>The system text key for the time format containing an hour, minute, second and millisecond.</summary>
public const string TimeHourMinuteSecondMs = "Tx:time.hour minute second ms";
/// <summary>The system text key for the time format containing an hour, minute, second and millisecond in tabular form (fixed-length).</summary>
public const string TimeHourMinuteSecondMsTab = "Tx:time.hour minute second ms.tab";
/// <summary>The system text key for the time format containing an hour, minute and second.</summary>
public const string TimeHourMinuteSecond = "Tx:time.hour minute second";
/// <summary>The system text key for the time format containing an hour, minute and second in tabular form (fixed-length).</summary>
public const string TimeHourMinuteSecondTab = "Tx:time.hour minute second.tab";
/// <summary>The system text key for the time format containing an hour and minute.</summary>
public const string TimeHourMinute = "Tx:time.hour minute";
/// <summary>The system text key for the time format containing an hour and minute in tabular form (fixed-length).</summary>
public const string TimeHourMinuteTab = "Tx:time.hour minute.tab";
/// <summary>The system text key for the time format containing an hour only.</summary>
public const string TimeHour = "Tx:time.hour";
/// <summary>The system text key for the time format containing an hour only in tabular form (fixed-length).</summary>
public const string TimeHourTab = "Tx:time.hour.tab";
/// <summary>The system text key for the time AM indicator.</summary>
public const string TimeAM = "Tx:time.am";
/// <summary>The system text key for the time PM indicator.</summary>
public const string TimePM = "Tx:time.pm";
/// <summary>The system text key for the separator between two levels of a relative time.</summary>
public const string TimeRelativeSeparator = "Tx:time.relative separator";
/// <summary>The system text key for the current time.</summary>
public const string TimeNow = "Tx:time.now";
/// <summary>The system text key for the unset time.</summary>
public const string TimeNever = "Tx:time.never";
/// <summary>The system text key for a relative point in time in the future. Uses the {interval} placeholder.</summary>
public const string TimeRelative = "Tx:time.relative";
/// <summary>The system text key for years of a relative point in time in the future. Uses the {#} count placeholder.</summary>
public const string TimeRelativeYears = "Tx:time.relative.years";
/// <summary>The system text key for months of a relative point in time in the future. Uses the {#} count placeholder.</summary>
public const string TimeRelativeMonths = "Tx:time.relative.months";
/// <summary>The system text key for days of a relative point in time in the future. Uses the {#} count placeholder.</summary>
public const string TimeRelativeDays = "Tx:time.relative.days";
/// <summary>The system text key for hours of a relative point in time in the future. Uses the {#} count placeholder.</summary>
public const string TimeRelativeHours = "Tx:time.relative.hours";
/// <summary>The system text key for minutes of a relative point in time in the future. Uses the {#} count placeholder.</summary>
public const string TimeRelativeMinutes = "Tx:time.relative.minutes";
/// <summary>The system text key for seconds of a relative point in time in the future. Uses the {#} count placeholder.</summary>
public const string TimeRelativeSeconds = "Tx:time.relative.seconds";
/// <summary>The system text key for a relative point in time in the past. Uses the {interval} placeholder.</summary>
public const string TimeRelativeNeg = "Tx:time.relative neg";
/// <summary>The system text key for years of a relative point in time in the past. Uses the {#} count placeholder.</summary>
public const string TimeRelativeNegYears = "Tx:time.relative neg.years";
/// <summary>The system text key for months of a relative point in time in the past. Uses the {#} count placeholder.</summary>
public const string TimeRelativeNegMonths = "Tx:time.relative neg.months";
/// <summary>The system text key for days of a relative point in time in the past. Uses the {#} count placeholder.</summary>
public const string TimeRelativeNegDays = "Tx:time.relative neg.days";
/// <summary>The system text key for hours of a relative point in time in the past. Uses the {#} count placeholder.</summary>
public const string TimeRelativeNegHours = "Tx:time.relative neg.hours";
/// <summary>The system text key for minutes of a relative point in time in the past. Uses the {#} count placeholder.</summary>
public const string TimeRelativeNegMinutes = "Tx:time.relative neg.minutes";
/// <summary>The system text key for seconds of a relative point in time in the past. Uses the {#} count placeholder.</summary>
public const string TimeRelativeNegSeconds = "Tx:time.relative neg.seconds";
/// <summary>The system text key for a relative time span into the future. Uses the {interval} placeholder.</summary>
public const string TimeSpanRelative = "Tx:time.relative span";
/// <summary>The system text key for years of a relative time span into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeYears = "Tx:time.relative span.years";
/// <summary>The system text key for months of a relative time span into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeMonths = "Tx:time.relative span.months";
/// <summary>The system text key for days of a relative time span into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeDays = "Tx:time.relative span.days";
/// <summary>The system text key for hours of a relative time span into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeHours = "Tx:time.relative span.hours";
/// <summary>The system text key for minutes of a relative time span into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeMinutes = "Tx:time.relative span.minutes";
/// <summary>The system text key for seconds of a relative time span into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeSeconds = "Tx:time.relative span.seconds";
/// <summary>The system text key for a relative time span into the past. Uses the {interval} placeholder.</summary>
public const string TimeSpanRelativeNeg = "Tx:time.relative span neg";
/// <summary>The system text key for years of a relative time span into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeNegYears = "Tx:time.relative span neg.years";
/// <summary>The system text key for months of a relative time span into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeNegMonths = "Tx:time.relative span neg.months";
/// <summary>The system text key for days of a relative time span into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeNegDays = "Tx:time.relative span neg.days";
/// <summary>The system text key for hours of a relative time span into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeNegHours = "Tx:time.relative span neg.hours";
/// <summary>The system text key for minutes of a relative time span into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeNegMinutes = "Tx:time.relative span neg.minutes";
/// <summary>The system text key for seconds of a relative time span into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanRelativeNegSeconds = "Tx:time.relative span neg.seconds";
/// <summary>The system text key for a relative time span going into the future. Uses the {interval} placeholder.</summary>
public const string TimeSpan = "Tx:time.span";
/// <summary>The system text key for years of a relative time span going into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanYears = "Tx:time.span.years";
/// <summary>The system text key for months of a relative time span going into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanMonths = "Tx:time.span.months";
/// <summary>The system text key for days of a relative time span going into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanDays = "Tx:time.span.days";
/// <summary>The system text key for hours of a relative time span going into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanHours = "Tx:time.span.hours";
/// <summary>The system text key for minutes of a relative time span going into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanMinutes = "Tx:time.span.minutes";
/// <summary>The system text key for seconds of a relative time span going into the future. Uses the {#} count placeholder.</summary>
public const string TimeSpanSeconds = "Tx:time.span.seconds";
/// <summary>The system text key for a relative time span going into the past. Uses the {interval} placeholder.</summary>
public const string TimeSpanNeg = "Tx:time.span neg";
/// <summary>The system text key for years of a relative time span going into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanNegYears = "Tx:time.span neg.years";
/// <summary>The system text key for months of a relative time span going into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanNegMonths = "Tx:time.span neg.months";
/// <summary>The system text key for days of a relative time span going into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanNegDays = "Tx:time.span neg.days";
/// <summary>The system text key for hours of a relative time span going into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanNegHours = "Tx:time.span neg.hours";
/// <summary>The system text key for minutes of a relative time span going into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanNegMinutes = "Tx:time.span neg.minutes";
/// <summary>The system text key for seconds of a relative time span going into the past. Uses the {#} count placeholder.</summary>
public const string TimeSpanNegSeconds = "Tx:time.span neg.seconds";
/// <summary>The system text key for combining items in a conjunctive (AND) enumeration.</summary>
public const string EnumAndCombiner = "Tx:enum.and.combiner";
/// <summary>The system text key for combining the last item in a conjunctive (AND) enumeration.</summary>
public const string EnumAndLastCombiner = "Tx:enum.and.last combiner";
/// <summary>The system text key for combining items in a disjunctive (OR) enumeration.</summary>
public const string EnumOrCombiner = "Tx:enum.or.combiner";
/// <summary>The system text key for combining the last item in a disjunctive (OR) enumeration.</summary>
public const string EnumOrLastCombiner = "Tx:enum.or.last combiner";
}
#endregion Constants
#region Global data
/// <summary>
/// Reader/writer lock object that controls access to the global dictionary and the
/// fileWatchers list.
/// </summary>
/// <remarks>
/// A note on locking access to the dictionary: There is two places where a read lock must
/// be acquired. One is the deepest level of helper functions. This is basically only
/// GetText which tries several cultures to find a text. This lock at the deepest level of
/// the call hierarchy ensures that every read access of the dictionary is secured. The
/// other place is every method that has multiple calls to functions that will eventually
/// access the dictionary. This is to ensure that every retrieved text belongs to the same
/// generation of texts and that the dictionary was not changed for example between the
/// opening and closing quotation mark. Other methods that only call GetText, ResolveData
/// or the like a single time need not be locked.
/// </remarks>
private static ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
/// <summary>
/// Contains all loaded languages associated with their culture name.
/// The first level defines the culture name.
/// The second level defines the text key.
/// The third level defines the quantifier.
/// The fourth level contains the actual text value.
/// </summary>
/// <remarks>
/// Access to this object is synchronised through rwlock.
/// </remarks>
private static Dictionary<string, Dictionary<string, Dictionary<int, string>>> languages =
new Dictionary<string, Dictionary<string, Dictionary<int, string>>>();
/// <summary>
/// Dictionary backup. Used to store the original data while the system texts are
/// temporarily replaced for TxEditor purposed for date and time preview.
/// </summary>
private static Dictionary<string, Dictionary<string, Dictionary<int, string>>> languagesBackup;
/// <remarks>
/// Access to this object is synchronised through rwlock.
/// </remarks>
private static bool useFileSystemWatcher;
/// <remarks>
/// Access to this object is synchronised through rwlock.
/// </remarks>
private static string primaryCulture;
/// <summary>
/// A list of all culture names accepted by the browser, sorted by descending preference.
/// This must be updated by the calling ASP.NET application for every page request from the
/// HTTP_ACCEPT_LANGUAGE header value through the SetWebCulture method.
/// </summary>
/// <remarks>
/// No locking is required for this field because every thread has its own instance.
/// </remarks>
[ThreadStatic]
private static string[] httpPreferredCultures;
/// <summary>
/// Contains a FileSystemWatcher instance monitoring each loaded file for changes.
/// </summary>
/// <remarks>
/// Access to this object is synchronised through rwlock.
/// </remarks>
private static Dictionary<string, FileSystemWatcher> fileWatchers =
new Dictionary<string, FileSystemWatcher>();
private static object reloadTimerLock = new object();
/// <remarks>
/// Access to this object is synchronised through reloadTimerLock.
/// </remarks>
private static Timer reloadTimer;
/// <summary>
/// Contains the last write time of every loaded file to compare it for updates.
/// </summary>
/// <remarks>
/// Access to this object is synchronised through rwlock.
/// </remarks>
private static Dictionary<string, DateTime> fileTimes =
new Dictionary<string, DateTime>();
private static object logLock = new object();
/// <summary>
/// File name to write log messages to. If it is empty (but not null), Trace is the log target.
/// </summary>
/// <remarks>
/// Access to this object is synchronised through logLock.
/// </remarks>
private static string logFileName;
/// <remarks>
/// Access to this object is synchronised through logLock.
/// </remarks>
private static StreamWriter logWriter;
/// <summary>
/// Contains all text keys that have been requested. Used to determine unused text keys.
/// </summary>
/// <remarks>
/// Access to this object is synchronised through logLock.
/// </remarks>
private static HashSet<string> usedKeys;
#endregion Global data
#region Events
/// <summary>
/// Fired when the dictionary has changed.
/// </summary>
public static event EventHandler DictionaryChanged;
private static void RaiseDictionaryChanged()
{
EventHandler handler = DictionaryChanged;
if (handler != null)
{
// Sanity check to prevent later failures
if (rwlock.IsWriteLockHeld)
throw new InvalidOperationException("Internal error: DictionaryChanged event raised with writer lock still set.");
handler(null, EventArgs.Empty);
}
}
#endregion Events
#region Static constructor
/// <summary>
/// Initialises the Tx class.
/// </summary>
static Tx()
{
LogFileName =
Environment.GetEnvironmentVariable(LogFileEnvironmentVariableName) ??
Environment.GetEnvironmentVariable(LogFileEnvironmentVariableName, EnvironmentVariableTarget.User);
if (Environment.GetEnvironmentVariable(LogUnusedEnvironmentVariableName) == "1" ||
Environment.GetEnvironmentVariable(LogUnusedEnvironmentVariableName, EnvironmentVariableTarget.User) == "1")
{
usedKeys = new HashSet<string>();
}
}
#endregion Static constructor
#region Properties
/// <summary>
/// Gets or sets a value indicating whether the loaded files should be monitored for
/// changes using FileSystemWatcher instances and reloaded automatically. Only files loaded
/// after setting this property can be monitored.
/// </summary>
public static bool UseFileSystemWatcher
{
get
{
using (new ReadLock(rwlock))
{
return useFileSystemWatcher;
}
}
set
{
using (new UpgradeableReadLock(rwlock))
{
if (value != useFileSystemWatcher)
{
using (new WriteLock(rwlock))
{
useFileSystemWatcher = value;
if (!useFileSystemWatcher)
{
// Dispose all created FileSystemWatcher instances for the previously loaded files
foreach (FileSystemWatcher fsw in fileWatchers.Values)
{
fsw.Dispose();
}
fileWatchers.Clear();
lock (reloadTimerLock)
{
if (reloadTimer != null)
{
// Cancel running timer (pending callbacks may still be invoked)
reloadTimer.Dispose();
reloadTimer = null;
}
}
}
}
}
}
}
}
private static TxMode globalMode;
[ThreadStatic]
private static TxMode threadMode;
/// <summary>
/// Gets or sets the operation mode for the current application domain.
/// </summary>
public static TxMode GlobalMode
{
get { return globalMode; }
set { globalMode = value; }
}
/// <summary>
/// Gets or sets the operation mode for the current thread.
/// </summary>
public static TxMode ThreadMode
{
get { return threadMode; }
set { threadMode = value; }
}
#endregion Properties
#region Load methods
/// <summary>
/// Loads all XML files in a directory into the global dictionary.
/// </summary>
/// <param name="path">Directory to load files from.</param>
/// <param name="filePrefix">File name prefix to limit loading to.</param>
/// <remarks>
/// <para>
/// This method searches the directory for files that end with the extension .txd (for Tx
/// dictionary) and an optional culture name directly before the extension. If filePrefix
/// is set, the matching file names additionally need to begin with this prefix and may not
/// contain additional characters between the prefix and the culture and extension. For
/// example, the files "demo.en-us.txd" and "demo.txd" match the prefix "demo" as well as
/// no prefix at all, but no other prefix.
/// </para>
/// <para>
/// Since the filePrefix value is directly used in a regular expression, it should not
/// contain characters that are special to a regular expression pattern. Safe characters
/// include letters, digits, underline (_) and hyphen (-). You may use regular expression
/// syntax to specify wildcards, for example ".*".
/// </para>
/// </remarks>
public static void LoadDirectory(string path, string filePrefix = null)
{
string regex = @"(\.(([a-z]{2})([-][a-z]{2})?))?\.(?:txd|xml)$";
if (!string.IsNullOrEmpty(filePrefix))
{
regex = "^" + filePrefix + regex;
}
using (new WriteLock(rwlock))
{
foreach (string fileName in Directory.GetFiles(path))
{
Match m = Regex.Match(Path.GetFileName(fileName), regex, RegexOptions.IgnoreCase);
if (m.Success)
{
LoadFromXmlFile(fileName);
}
}
}
}
/// <summary>
/// Loads all text definitions from an XML file into the global dictionary.
/// </summary>
/// <param name="fileName">Name of the XML file to load.</param>
/// <remarks>
/// <para>
/// If the file name ends with ".[culture name].txd", then the culture name of the file
/// name is used. Otherwise a combined file is assumed that contains a "culture" element
/// with a "name" attribute for each defined culture.
/// </para>
/// <para>
/// Culture names follow the format that the CultureInfo class supports and are actually
/// validated that way. Examples are "de", "en", "de-de", "en-us" or "pt-br". Culture
/// names are handled case-insensitively for this method.
/// </para>
/// </remarks>
public static void LoadFromXmlFile(string fileName)
{
using (new WriteLock(rwlock))
{
LoadFromXmlFile(fileName, languages);
}
}
/// <summary>
/// Loads all text definitions from an XML file into a dictionary.
/// </summary>
/// <param name="fileName">Name of the XML file to load.</param>
/// <param name="dict">Dictionary to load the texts into.</param>
private static void LoadFromXmlFile(string fileName, Dictionary<string, Dictionary<string, Dictionary<int, string>>> dict)
{
if (!Path.IsPathRooted(fileName))
{
fileName = Path.GetFullPath(fileName);
}
using (new WriteLock(rwlock))
{
if (UseFileSystemWatcher)
{
// Monitor this file to automatically reload it when it is changed
if (!fileWatchers.ContainsKey(fileName))
{
FileSystemWatcher fsw = new FileSystemWatcher(Path.GetDirectoryName(fileName), Path.GetFileName(fileName));
fsw.InternalBufferSize = 4096; // Minimum possible value
fsw.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size | NotifyFilters.FileName;
fsw.Changed += fsw_Changed;
fsw.Created += fsw_Changed;
fsw.Renamed += fsw_Changed;
fsw.EnableRaisingEvents = true;
fileWatchers[fileName] = fsw;
}
}
// Remember last write time of the file to be able to compare it later
fileTimes[fileName] = new FileInfo(fileName).LastWriteTimeUtc;
}
// First load the XML file into an XmlDocument for further processing
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(fileName);
// Try to recognise the culture name from the file name
Match m = Regex.Match(fileName, @"\.(([a-z]{2})([-][a-z]{2})?)\.(?:txd|xml)$", RegexOptions.IgnoreCase);
if (m.Success)
{
CultureInfo ci = CultureInfo.GetCultureInfo(m.Groups[1].Value);
LoadFromXml(ci.Name, xmlDoc.DocumentElement, dict);
// Set the primary culture if a file claims to be it
XmlAttribute primaryAttr = xmlDoc.DocumentElement.Attributes["primary"];
if (primaryAttr != null && primaryAttr.Value == "true")
{
PrimaryCulture = ci.Name;
}
return;
}
// Try to find the culture name inside a combined XML document
foreach (XmlElement xe in xmlDoc.DocumentElement.SelectNodes("culture[@name]"))
{
CultureInfo ci = CultureInfo.GetCultureInfo(xe.Attributes["name"].Value);
LoadFromXml(ci.Name, xe, dict);
// Set the primary culture if a culture in the file claims to be it
XmlAttribute primaryAttr = xe.Attributes["primary"];
if (primaryAttr != null && primaryAttr.Value == "true")
{
PrimaryCulture = ci.Name;
}
}
}
private static void fsw_Changed(object sender, FileSystemEventArgs e)
{
// A Renamed event is called twice when saving the file with TxEditor. The first
// renaming is from .txd to .txd.bak when creating the original backup file. This does
// not change the loaded dictionary file actually. The second renaming is from .txd.tmp
// to .txd when safe-writing the new dictionary file. This happens directly after the
// first event, before the reload timer has elapsed, so it doesn't hurt to handle both
// events.
lock (reloadTimerLock)
{
if (reloadTimer != null)
{
// Cancel running timer (pending callbacks may still be invoked)
reloadTimer.Dispose();
}
reloadTimer = new Timer(reloadTimer_Callback, null, ReloadChangesDelay, Timeout.Infinite);
}
}
private static void reloadTimer_Callback(object state)
{
lock (reloadTimerLock)
{
if (reloadTimer == null) return; // Timer has been cancelled, do nothing
reloadTimer.Dispose();
reloadTimer = null;
}
try
{
// Read all loaded files into a temporary dictionary
Dictionary<string, Dictionary<string, Dictionary<int, string>>> newLanguages =
new Dictionary<string, Dictionary<string, Dictionary<int, string>>>();
foreach (string fileName in new List<string>(fileTimes.Keys))
{
LoadFromXmlFile(fileName, newLanguages);
}
// Replace the global dictionary with the new one to apply the new texts
using (new WriteLock(rwlock))
{
languages = newLanguages;
}
Log("{0} files reloaded.", fileTimes.Count);
RaiseDictionaryChanged();
}
catch (Exception ex)
{
// Catch and log any exceptions, Visual Studio will instantly terminate debugging
// if something unexcepted happens in this thread.
// TODO: Test behaviour with a global unhandled exception handler in place.
System.Diagnostics.Trace.WriteLine("Unhandled " + ex.GetType().Name + " while reloading the dictionary files: " + ex.Message);
}
}
/// <summary>
/// Loads all text definitions from an embedded resource XML file into the global
/// dictionary. Only the combined format with all cultures in one document is supported by
/// this method.
/// </summary>
/// <param name="name">Name of the embedded resource file to load.</param>
/// <remarks>
/// The resource name is the project's default namespace and the file path relative to the
/// project, combined with dots (.) and all path separators also replaced with dots.
/// </remarks>
public static void LoadFromEmbeddedResource(string name)
{
// First load the XML file into an XmlDocument for further processing
Stream stream = System.Reflection.Assembly.GetCallingAssembly().GetManifestResourceStream(name);
if (stream == null)
{
throw new ArgumentException("The embedded resource name was not found in the calling assembly.");
}
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(stream);
// Try to find the culture name inside a combined XML document
using (new WriteLock(rwlock))
{
foreach (XmlElement xe in xmlDoc.DocumentElement.SelectNodes("culture[@name]"))
{
CultureInfo ci = CultureInfo.GetCultureInfo(xe.Attributes["name"].Value);
LoadFromXml(ci.Name, xe, languages);
// Set the primary culture if a culture in the file claims to be it
XmlAttribute primaryAttr = xe.Attributes["primary"];
if (primaryAttr != null && primaryAttr.Value == "true")
{
PrimaryCulture = ci.Name;
}
}
}
// Need to raise the event after releasing the write lock
RaiseDictionaryChanged();
}
/// <summary>
/// Loads all text definitions from an XML element into a dictionary.
/// </summary>
/// <param name="culture">Culture name to add the text definitions to. Must be the exact name of an existing culture in .NET.</param>
/// <param name="xe">XML element to read the text definitions from.</param>
/// <param name="dict">Dictionary to add texts to.</param>
private static void LoadFromXml(string culture, XmlElement xe, Dictionary<string, Dictionary<string, Dictionary<int, string>>> dict)
{
// We only need the lock if we're writing directly to the global dictionary
bool isGlobalDict;
using (new ReadLock(rwlock))
{
isGlobalDict = dict == languages;
}
using (new WriteLock(isGlobalDict ? rwlock : null))
{
// Get a reference to the specified language dictionary
Dictionary<string, Dictionary<int, string>> language;
if (!dict.TryGetValue(culture, out language))
{
language = new Dictionary<string, Dictionary<int, string>>();
dict.Add(culture, language);
}
// Read the XML document
foreach (XmlNode textNode in xe.SelectNodes("text[@key]"))
{
string text = textNode.InnerText;
string key = textNode.Attributes["key"].Value;
if (key == "")
{
Log("Load XML: Key attribute is empty. Ignoring definition.");
continue;
}
int count = -1;
XmlAttribute countAttr = textNode.Attributes["count"];
if (countAttr != null)
{
if (!int.TryParse(countAttr.Value, out count))
{
// Count value unparsable. Skip invalid entries
Log("Load XML: Count attribute value of key {0} is not an integer. Ignoring definition.", key);
continue;
}
if (count < 0 || count > ushort.MaxValue)
{
// Count value out of range. Skip invalid entries
Log("Load XML: Count attribute value of key {0} is out of range. Ignoring definition.", key);
continue;
}
}
int modulo = 0;
XmlAttribute moduloAttr = textNode.Attributes["mod"];
if (moduloAttr != null)
{
if (!int.TryParse(moduloAttr.Value, out modulo))
{
// Modulo value unparsable. Skip invalid entries
Log("Load XML: Modulo attribute of key {0} is not an integer. Ignoring definition.", key);
continue;
}
if (modulo < 2 || modulo > 1000)
{
// Modulo value out of range. Skip invalid entries
Log("Load XML: Modulo attribute of key {0} is out of range. Ignoring definition.", key);
continue;
}
}
Dictionary<int, string> textItem;
if (language.TryGetValue(key, out textItem))
{
// Key has already been read, add the new text item.
// Existing text items are overwritten.
if (count != -1 && modulo != 0)
{
// Encode the modulo value into the quantifier.
count = (modulo << 16) | count;
}
textItem[count] = text;
}
else
{
// New key.
textItem = new Dictionary<int, string>();
textItem.Add(count, text);
language.Add(key, textItem);
}
}
}
if (isGlobalDict && !rwlock.IsWriteLockHeld)
{
// Raise the changed event if the texts have been loaded into the global
// dictionary, but not if a write lock is held because then, nothing could be read
// from the dictionary by others. In a situation where this method is called with
// a write lock, the caller must raise the changed event after releasing the lock.
RaiseDictionaryChanged();
}
}
/// <summary>
/// Checks the last write time of all loaded files and reloads all files if something has
/// changed. This method should be called to reload the text files if necessary and only if
/// UseFileSystemWatcher is false. In an ASP.NET environment, this could be a new page
/// loading.
/// </summary>
public static void CheckReloadFiles()
{
using (new UpgradeableReadLock(rwlock))
{
bool changed = false;
if (languages.Count == 0)
{
// Sometimes this seems to happen in an ASP.NET environment.
changed = true;
}
else
{
try
{
foreach (KeyValuePair<string, DateTime> kvp in fileTimes)
{
if (new FileInfo(kvp.Key).LastWriteTimeUtc > kvp.Value)
{
changed = true;
break;
}
}
}
catch
{
changed = true;
}
}
if (changed)
{
// Read all known files into a temporary dictionary
Dictionary<string, Dictionary<string, Dictionary<int, string>>> newLanguages =
new Dictionary<string, Dictionary<string, Dictionary<int, string>>>();
foreach (string fileName in new List<string>(fileTimes.Keys))
{
LoadFromXmlFile(fileName, newLanguages);
}
// Replace the global dictionary with the new one to apply the new texts
using (new WriteLock(rwlock))
{
languages = newLanguages;
}
RaiseDictionaryChanged();
}
}
}
/// <summary>
/// Adds a text to the dictionary.
/// </summary>
/// <param name="culture">New or existing culture name to add the text definition to. Must be the exact name of an existing culture in .NET.</param>
/// <param name="key">Text key to add or update.</param>
/// <param name="text">Text value to add.</param>
public static void AddText(string culture, string key, string text)
{
AddText(culture, key, -1, 0, text);
}
/// <summary>
/// Adds a text to the dictionary.
/// </summary>
/// <param name="culture">New or existing culture name to add the text definition to. Must be the exact name of an existing culture in .NET.</param>
/// <param name="key">Text key to add or update.</param>
/// <param name="count">Count value for the text.</param>
/// <param name="text">Text value to add.</param>
public static void AddText(string culture, string key, int count, string text)
{
AddText(culture, key, count, 0, text);
}
/// <summary>
/// Adds a text to the dictionary.
/// </summary>
/// <param name="culture">New or existing culture name to add the text definition to. Must be the exact name of an existing culture in .NET.</param>
/// <param name="key">Text key to add or update.</param>
/// <param name="count">Count value for the text.</param>
/// <param name="modulo">Modulo value for the text.</param>
/// <param name="text">Text value to add.</param>
public static void AddText(string culture, string key, int count, int modulo, string text)
{
if (count < -1 || count > ushort.MaxValue)
{
throw new ArgumentOutOfRangeException("count", "The count value must be in the range of 0 to ushort.MaxValue.");
}
if (modulo != 0 && (modulo < 2 || modulo > 1000))
{
throw new ArgumentOutOfRangeException("modulo", "The modulo value must be in the range of 2 to 1000.");
}
if (count == -1 && modulo > 0)
{
throw new ArgumentException("A modulo value cannot be used if no count value is set.");
}
using (new WriteLock(rwlock))
{
// Get a reference to the specified language dictionary
Dictionary<string, Dictionary<int, string>> language;
if (!languages.TryGetValue(culture, out language))
{
language = new Dictionary<string, Dictionary<int, string>>();
languages.Add(culture, language);
}
Dictionary<int, string> textItem;
if (language.TryGetValue(key, out textItem))
{
// Key is already defined, add the new text item.
// Existing text items are overwritten.
if (count != -1 && modulo != 0)
{
// Encode the modulo value into the quantifier.
count = (modulo << 16) | count;
}
textItem[count] = text;
}
else
{
// New key.
textItem = new Dictionary<int, string>();
textItem.Add(count, text);
language.Add(key, textItem);
}
}
RaiseDictionaryChanged();
}
/// <summary>
/// Clears the global dictionary and removes all currently loaded languages and texts.
/// </summary>
public static void Clear()
{
using (new WriteLock(rwlock))
{
// Dispose all created FileSystemWatcher instances for the previously loaded files
foreach (FileSystemWatcher fsw in fileWatchers.Values)
{
fsw.Dispose();
}
fileWatchers.Clear();
fileTimes.Clear();
languages.Clear();
}
RaiseDictionaryChanged();
}
/// <summary>
/// Replaces the system keys (Tx:*) by the provided texts. This is only intended to be used
/// by the date and time preview in TxEditor.
/// </summary>
/// <param name="systemTexts"></param>
public static void ReplaceSystemTexts(Dictionary<string, Dictionary<string, Dictionary<int, string>>> systemTexts)
{
// Keep a backup of the original data to restore it later
if (languagesBackup == null)
{