├── LICENSE ├── LocalizationTracker ├── App.xaml ├── App.xaml.cs ├── AppConfig.cs ├── Components │ ├── Authorization.cs │ ├── ComboBoxExtensionMethods.cs │ ├── ComboBoxWidthFromItemsBehavior.cs │ ├── InlineTemplate.cs │ ├── InlineTemplatesHelper.cs │ ├── InlinesTextBlock.cs │ ├── InlinesWrapper.cs │ ├── LocaleDetailsView.xaml │ ├── LocaleDetailsView.xaml.cs │ ├── RepoConnection.cs │ ├── StringDetailsView.xaml │ ├── StringDetailsView.xaml.cs │ ├── TargetSelector.xaml │ ├── TargetSelector.xaml.cs │ ├── TargetSelectorEntry.cs │ ├── TraitDetailsView.xaml │ ├── TraitDetailsView.xaml.cs │ ├── TraitSelectorEntry.cs │ ├── TraitsSelector.xaml │ └── TraitsSelector.xaml.cs ├── Data │ ├── BoundsHelper.cs │ ├── ExportRequestResult.cs │ ├── ExportResults.cs │ ├── ImportEntry.cs │ ├── ImportFileResult.cs │ ├── ImportRequestResult.cs │ ├── JsonLocalizationData.cs │ ├── LocaleEntry.cs │ ├── Shared │ │ ├── Locale.cs │ │ ├── LocaleTrait.cs │ │ ├── LocalizedStringData.cs │ │ ├── StringTrait.cs │ │ └── TraitData.cs │ ├── StringEntry.cs │ ├── StringEntryComparison.cs │ ├── TagsList.cs │ ├── Unreal │ │ ├── StringsArchiveUnreal.cs │ │ ├── UnrealLocaleData.cs │ │ ├── UnrealStringData.cs │ │ └── UnrealTraitData.cs │ └── Wrappers │ │ ├── ILocaleData.cs │ │ ├── IStringData.cs │ │ ├── ITraitData.cs │ │ ├── StringsArchive.cs │ │ ├── StringsArchiveUnity.cs │ │ └── TextFixupHelper.cs ├── GlobalUsings.cs ├── LocalizationTracker.csproj ├── Logic │ ├── DublicatesResolver.cs │ ├── Excel │ │ ├── ExcelExctentions.cs │ │ ├── ExcelImporter.cs │ │ ├── ExcelStyles.cs │ │ ├── ExcelStylesCreator.cs │ │ ├── ExcelTagMismatchStyle.cs │ │ ├── ExportWrapper.cs │ │ ├── Exporters │ │ │ ├── ExcelLocalizationExporter.cs │ │ │ ├── ExcelSourceUpdateExporter.cs │ │ │ ├── ExcelTagsMismathExporter.cs │ │ │ ├── SoundExporter.cs │ │ │ ├── SpeakersExport.cs │ │ │ └── VoiceCommentsExport.cs │ │ ├── IExcelStyler.cs │ │ ├── SheetDataExtentions.cs │ │ ├── SheetWrapper.cs │ │ ├── SpreadsheetHelper.cs │ │ ├── WorkbookWrapper.cs │ │ └── Wrappers │ │ │ ├── ExportHierarchyWrapper.cs │ │ │ └── ExportSingleFileWrapper.cs │ ├── ImportDiffExporter.cs │ ├── JsonImporter.cs │ ├── KnownTraits.cs │ ├── LocalizationDiffExporter.cs │ ├── LocalizationExporter.cs │ ├── LocalizationImporter.cs │ ├── OpenOffice │ │ ├── OpenOfficeExporter.cs │ │ ├── OpenOfficeImporter.cs │ │ └── OpenOfficeXmlHelper.cs │ ├── StringManager.cs │ ├── TranslateUtility.cs │ └── VoiceCommentsImporter.cs ├── OwlcatProtocolListener.cs ├── Properties │ ├── PublishProfiles │ │ └── FolderProfile.pubxml │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ ├── Settings.settings │ └── launchSettings.json ├── Tools │ ├── Diff.cs │ ├── FilterFits.cs │ ├── GlossaryTools │ │ ├── DataStructure │ │ │ ├── Glossary.cs │ │ │ ├── TermEntry.cs │ │ │ ├── TermTemplate.cs │ │ │ └── TermTemplateCollection.cs │ │ ├── Json │ │ │ ├── BakingSheet.Json.asmdef │ │ │ ├── BakingSheet.Json.asmdef.meta │ │ │ ├── JsonSheetAssetPathConverter.cs │ │ │ ├── JsonSheetAssetPathConverter.cs.meta │ │ │ ├── JsonSheetContractResolver.cs │ │ │ ├── JsonSheetContractResolver.cs.meta │ │ │ ├── JsonSheetConverter.cs │ │ │ ├── JsonSheetConverter.cs.meta │ │ │ ├── JsonSheetReferenceConverter.cs │ │ │ └── JsonSheetReferenceConverter.cs.meta │ │ └── SheetImport │ │ │ ├── GlossarySheet.cs │ │ │ ├── GlossarySheetContainer.cs │ │ │ ├── Logger.cs │ │ │ └── SheetImporter.cs │ ├── MaxLength.cs │ ├── SVGTool │ │ ├── GenerateSVG.cs │ │ └── SvgWriter.cs │ └── SpellCheck.cs ├── Utility │ ├── ColorUtility.cs │ ├── GenderSystem.cs │ ├── JsonSerializerHelpers.cs │ ├── LinqExtension.cs │ ├── ObservableRangeCollection.cs │ ├── RepairKit.cs │ ├── StopWatchUtils.cs │ ├── StringUtils.cs │ ├── SystemExtentions.cs │ ├── UnityAssets.cs │ └── WinFormsUtility.cs ├── ViewModel │ ├── FolderItemTreeModel.cs │ ├── LocaleDetailsVM.cs │ ├── SpeakersChangedVM.cs │ ├── StringDetailsVM.cs │ └── TraitDetailsVM.cs ├── Windows │ ├── AuthorizationWindow.xaml │ ├── AuthorizationWindow.xaml.cs │ ├── ChangeTraitsDialog.xaml │ ├── ChangeTraitsDialog.xaml.cs │ ├── DublicateResolverWindow.xaml │ ├── DublicateResolverWindow.xaml.cs │ ├── ExportDialog.xaml │ ├── ExportDialog.xaml.cs │ ├── ExportResultsWindow.xaml │ ├── ExportResultsWindow.xaml.cs │ ├── FilterMode.cs │ ├── ImportErrorWindow.xaml │ ├── ImportErrorWindow.xaml.cs │ ├── ImportResultsWindow.xaml │ ├── ImportResultsWindow.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── MultilineSearch.xaml │ ├── MultilineSearch.xaml.cs │ ├── ProgressReporter.cs │ ├── ProgressWindow.xaml │ ├── ProgressWindow.xaml.cs │ ├── SpeakerChangedWindow.xaml │ ├── SpeakerChangedWindow.xaml.cs │ ├── StringDetailsWindow.xaml │ ├── StringDetailsWindow.xaml.cs │ ├── StringsFilter.cs │ └── TraitsFilter.cs └── config.json └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Owlcat Games 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LocalizationTracker/App.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/ComboBoxExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Automation.Peers; 3 | using System.Windows.Automation.Provider; 4 | using System.Windows.Controls.Primitives; 5 | using System.Windows.Controls; 6 | using System.Windows; 7 | 8 | namespace LocalizationTracker.Components 9 | { 10 | public static class ComboBoxExtensionMethods 11 | { 12 | public static void SetWidthFromItems(this ComboBox comboBox) 13 | { 14 | double comboBoxWidth = 19;// comboBox.DesiredSize.Width; 15 | 16 | // Create the peer and provider to expand the comboBox in code behind. 17 | ComboBoxAutomationPeer peer = new ComboBoxAutomationPeer(comboBox); 18 | IExpandCollapseProvider provider = (IExpandCollapseProvider)peer.GetPattern(PatternInterface.ExpandCollapse); 19 | EventHandler? eventHandler = null; 20 | eventHandler = new EventHandler(delegate 21 | { 22 | if (comboBox.IsDropDownOpen && 23 | comboBox.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) 24 | { 25 | double width = 0; 26 | foreach (var item in comboBox.Items) 27 | { 28 | ComboBoxItem comboBoxItem = (ComboBoxItem)comboBox.ItemContainerGenerator.ContainerFromItem(item); 29 | comboBoxItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); 30 | if (comboBoxItem.DesiredSize.Width > width) 31 | { 32 | width = comboBoxItem.DesiredSize.Width; 33 | } 34 | } 35 | comboBox.Width = comboBoxWidth + width; 36 | // Remove the event handler. 37 | comboBox.ItemContainerGenerator.StatusChanged -= eventHandler; 38 | comboBox.DropDownOpened -= eventHandler; 39 | provider.Collapse(); 40 | } 41 | }); 42 | comboBox.ItemContainerGenerator.StatusChanged += eventHandler; 43 | comboBox.DropDownOpened += eventHandler; 44 | // Expand the comboBox to generate all its ComboBoxItem's. 45 | provider.Expand(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/ComboBoxWidthFromItemsBehavior.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Threading; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | 6 | namespace LocalizationTracker.Components 7 | { 8 | public static class ComboBoxWidthFromItemsBehavior 9 | { 10 | public static readonly DependencyProperty ComboBoxWidthFromItemsProperty = 11 | DependencyProperty.RegisterAttached 12 | ( 13 | "ComboBoxWidthFromItems", 14 | typeof(bool), 15 | typeof(ComboBoxWidthFromItemsBehavior), 16 | new UIPropertyMetadata(false, OnComboBoxWidthFromItemsPropertyChanged) 17 | ); 18 | public static bool GetComboBoxWidthFromItems(DependencyObject obj) 19 | { 20 | return (bool)obj.GetValue(ComboBoxWidthFromItemsProperty); 21 | } 22 | public static void SetComboBoxWidthFromItems(DependencyObject obj, bool value) 23 | { 24 | obj.SetValue(ComboBoxWidthFromItemsProperty, value); 25 | } 26 | private static void OnComboBoxWidthFromItemsPropertyChanged(DependencyObject dpo, 27 | DependencyPropertyChangedEventArgs e) 28 | { 29 | ComboBox? comboBox = dpo as ComboBox; 30 | if (comboBox != null) 31 | { 32 | if ((bool)e.NewValue == true) 33 | { 34 | comboBox.Loaded += OnComboBoxLoaded; 35 | } 36 | else 37 | { 38 | comboBox.Loaded -= OnComboBoxLoaded; 39 | } 40 | } 41 | } 42 | private static void OnComboBoxLoaded(object sender, RoutedEventArgs e) 43 | { 44 | ComboBox comboBox = sender as ComboBox; 45 | Action action = () => { comboBox.SetWidthFromItems(); }; 46 | comboBox.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/InlineTemplate.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Documents; 3 | using System.Windows.Media; 4 | using static LocalizationTracker.Components.InlineTemplate; 5 | 6 | namespace LocalizationTracker.Components; 7 | 8 | public record struct InlineTemplate( 9 | string Text, 10 | Color? Foreground = null, 11 | Color? Background = null, 12 | bool StrikeThrough = false, 13 | bool Underline = false, 14 | FontWeight? FontWeight = null, 15 | InlineType InlineType = InlineType.Default) 16 | { 17 | public InlineTemplate(string text, Color? foreground, Color? background, bool strikeThrough, FontWeight? fontWeight) 18 | :this(text, foreground, background, strikeThrough, false, fontWeight) 19 | { 20 | } 21 | 22 | public Inline MakeInline() 23 | { 24 | var run = new Run(Text); 25 | if (Foreground != null) 26 | run.Foreground = new SolidColorBrush(Foreground.Value); 27 | if (Background != null) 28 | run.Background = new SolidColorBrush(Background.Value); 29 | if (StrikeThrough) 30 | run.TextDecorations.Add(TextDecorations.Strikethrough); 31 | if (FontWeight != null) 32 | run.FontWeight = FontWeight.Value; 33 | if (Underline) 34 | run.TextDecorations.Add(GetUnderlineDecoration()); 35 | return run; 36 | } 37 | 38 | private TextDecoration GetUnderlineDecoration() 39 | { 40 | TextDecoration myUnderline = new(); 41 | Pen myPen = new(); 42 | myPen.Brush = new SolidColorBrush(Colors.Blue); 43 | myPen.Brush.Opacity = 1; 44 | myPen.Thickness = 2; 45 | myUnderline.Pen = myPen; 46 | myUnderline.PenThicknessUnit = TextDecorationUnit.FontRecommended; 47 | return myUnderline; 48 | } 49 | } 50 | public enum InlineType 51 | { 52 | Default, 53 | DiffDelete, 54 | DiffInsert, 55 | SpellCheckError, 56 | GlossaryTerm, 57 | MaxLength 58 | } 59 | 60 | public enum InlineCollectionType 61 | { 62 | Default, 63 | TagsMismatch, 64 | DiffTrait, 65 | DiffSource, 66 | DiffSourceNoTags, 67 | SpellCheck, 68 | Filter, 69 | Glossary, 70 | MaxLength 71 | } -------------------------------------------------------------------------------- /LocalizationTracker/Components/InlineTemplatesHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace LocalizationTracker.Components; 6 | 7 | public static class InlineTemplatesHelper 8 | { 9 | public static InlinesWrapper MergeWith(this InlinesWrapper first, InlinesWrapper second) => 10 | new InlinesWrapper(Merge(first.InlineTemplates, second.InlineTemplates)); 11 | 12 | public static InlineTemplate[] Merge(InlineTemplate[] left, InlineTemplate[] right) 13 | { 14 | var leftLength = left.Sum(it => it.Text.Length); 15 | var rightLength = right.Sum(it => it.Text.Length); 16 | if (leftLength != rightLength) return left; 17 | 18 | IList current = new List(); 19 | 20 | var li = 0; // left 21 | var ri = 0; // right 22 | var ci = 0; // current 23 | 24 | var lp = 0; 25 | var rp = 0; 26 | var cp = 0; 27 | 28 | while (lp < leftLength) 29 | { 30 | var le = lp + left[li].Text.Length; 31 | var re = rp + right[ri].Text.Length; 32 | 33 | InlineTemplate newInline = new InlineTemplate("", 34 | right[ri].Foreground ?? left[li].Foreground , 35 | right[ri].Background ?? left[li].Background, 36 | right[ri].StrikeThrough ? right[ri].StrikeThrough : left[li].StrikeThrough, 37 | right[ri].Underline ? right[ri].Underline : left[li].Underline, 38 | right[ri].FontWeight ?? left[li].FontWeight, 39 | right[ri].InlineType != InlineType.Default ? right[ri].InlineType : left[li].InlineType 40 | ); 41 | 42 | if (lp < rp) // left start first 43 | { 44 | if (le < re) 45 | { 46 | newInline.Text = right[ri].Text.Substring(0, le - rp); 47 | lp = le; 48 | li++; 49 | } 50 | else 51 | { 52 | newInline.Text = right[ri].Text; 53 | rp = re; 54 | ri++; 55 | if (le == re) 56 | { 57 | lp = le; 58 | li++; 59 | } 60 | } 61 | } 62 | else 63 | { 64 | if (le < re) 65 | { 66 | newInline.Text = left[li].Text; 67 | lp = le; 68 | li++; 69 | } 70 | else 71 | { 72 | newInline.Text = left[li].Text.Substring(0, re - lp); 73 | rp = re; 74 | ri++; 75 | if (le == re) 76 | { 77 | lp = le; 78 | li++; 79 | } 80 | } 81 | } 82 | 83 | 84 | current.Add(newInline); 85 | } 86 | 87 | return current.ToArray(); 88 | } 89 | } -------------------------------------------------------------------------------- /LocalizationTracker/Components/InlinesTextBlock.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace LocalizationTracker.Components 5 | { 6 | public class InlinesTextBlock : TextBlock 7 | { 8 | public InlinesWrapper? InlinesWrapper { get; set; } 9 | 10 | public static readonly DependencyProperty LocaleEntryProperty = 11 | DependencyProperty.Register("InlinesWrapper", typeof(InlinesWrapper), typeof(InlinesTextBlock), new UIPropertyMetadata(null, InlinesContentPropertyChanged)); 12 | 13 | private static void InlinesContentPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) 14 | { 15 | if (sender is not InlinesTextBlock textBlock) 16 | return; 17 | 18 | textBlock.Inlines.Clear(); 19 | if (e.NewValue is not InlinesWrapper wrapper) 20 | return; 21 | 22 | textBlock.Inlines.AddRange(wrapper.Inlines); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /LocalizationTracker/Components/InlinesWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Windows; 6 | using System.Windows.Documents; 7 | using System.Xml; 8 | 9 | namespace LocalizationTracker.Components 10 | { 11 | public class InlinesWrapper 12 | { 13 | public IEnumerable Inlines => InlineTemplates.Select(v => v.MakeInline()); 14 | 15 | public readonly InlineTemplate[] InlineTemplates = Array.Empty(); 16 | 17 | public bool HasAny => InlineTemplates.Length > 0; 18 | 19 | public InlinesWrapper() 20 | { 21 | } 22 | 23 | public InlinesWrapper(string text) 24 | { 25 | if (!string.IsNullOrEmpty(text)) 26 | InlineTemplates = new[] { new InlineTemplate(text) }; 27 | } 28 | 29 | public InlinesWrapper(InlineTemplate[] inlines) 30 | { 31 | InlineTemplates = inlines; 32 | } 33 | 34 | public override string ToString() 35 | { 36 | var sb = new StringBuilder(); 37 | foreach (var inline in InlineTemplates) 38 | { 39 | sb.Append(inline.Text); 40 | } 41 | 42 | return sb.ToString(); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /LocalizationTracker/Components/LocaleDetailsView.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | Locale 13 | 14 | 15 | 16 | Updated 17 | 18 | 19 | 20 | Text 21 | 22 | 23 | 24 | Comment 25 | 26 | 27 | 28 | 29 | 30 | Translated From 31 | 32 | 33 | 34 | Translation Date 35 | 36 | 37 | 38 | Original Text 39 | 40 | 41 | 42 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/LocaleDetailsView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace LocalizationTracker.Components 17 | { 18 | /// 19 | /// Interaction logic for LocaleDetails.xaml 20 | /// 21 | public partial class LocaleDetailsView : UserControl 22 | { 23 | public LocaleDetailsView() 24 | { 25 | InitializeComponent(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/StringDetailsView.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | Key 13 | 14 | 15 | 16 | Path 17 | 18 | 19 | 20 | Comment 21 | 22 | 23 | 24 | Kind 25 | 26 | 27 | 28 | Speaker 29 | 30 | 31 | 32 | Attachment 33 | 34 | 35 | 38 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/StringDetailsView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace LocalizationTracker.Components 17 | { 18 | /// 19 | /// Interaction logic for UserControl1.xaml 20 | /// 21 | public partial class StringDetailsView : UserControl 22 | { 23 | public StringDetailsView() 24 | { 25 | InitializeComponent(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/TargetSelector.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 34 | 35 | 36 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/TargetSelector.xaml.cs: -------------------------------------------------------------------------------- 1 | using StringsCollector.Data; 2 | using LocalizationTracker.Data; 3 | using LocalizationTracker.Windows; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.ObjectModel; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Windows; 10 | using System.Windows.Controls; 11 | using System.Windows.Input; 12 | 13 | namespace LocalizationTracker.Components 14 | { 15 | public partial class TargetSelector : UserControl 16 | { 17 | public ObservableCollection TargetEntries { get; } = new ObservableCollection(); 18 | 19 | 20 | public ObservableCollection TargetSource 21 | { 22 | get { return (ObservableCollection)GetValue(TargetSourceProperty); } 23 | set { SetValue(TargetSourceProperty, value); } 24 | } 25 | 26 | public static readonly DependencyProperty TargetSourceProperty = 27 | DependencyProperty.Register("TargetSource", typeof(ObservableCollection), typeof(TargetSelector), new PropertyMetadata(TargetSourcePropertyChanged)); 28 | 29 | public string SelectedTargetText 30 | { 31 | get { return (string)GetValue(SelectedTargetTextProperty); } 32 | set { SetValue(SelectedTargetTextProperty, value); } 33 | } 34 | 35 | public static readonly DependencyProperty SelectedTargetTextProperty = 36 | DependencyProperty.Register(nameof(SelectedTargetText), typeof(string), typeof(TargetSelector)); 37 | 38 | public TargetSelector() 39 | { 40 | InitializeComponent(); 41 | CheckedListBox.ItemsSource = TargetEntries; 42 | UpdateTargetsList(); 43 | } 44 | 45 | private void ButtonShowPopup_Click(object sender, RoutedEventArgs e) 46 | { 47 | PopupTraits.IsOpen = !PopupTraits.IsOpen; 48 | } 49 | 50 | private static void TargetSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) 51 | { 52 | var selector = sender as TargetSelector; 53 | selector?.UpdateTargetsList(); 54 | } 55 | 56 | private void UpdateTargetsList() 57 | { 58 | TargetEntries.Clear(); 59 | 60 | foreach (var line in Locale.Values) 61 | { 62 | if (line == StringEntry.TargetLocale) 63 | { 64 | TargetEntries.Add(new TargetSelectorEntry(line, true)); 65 | SelectedTargetText = string.Join(", ", SelectedTargets); 66 | ButtonText.Text = $"{SelectedTargetText}"; 67 | } 68 | else 69 | { 70 | TargetEntries.Add(new TargetSelectorEntry(line, false)); 71 | } 72 | } 73 | 74 | CheckedListBox.SelectedItem = TargetEntries.FirstOrDefault(); 75 | 76 | if (TargetSource != null) 77 | { 78 | foreach (var e in TargetEntries) 79 | { 80 | e.Selected = TargetSource.Contains(e.Target); 81 | } 82 | ElementSelected(null, null); 83 | } 84 | 85 | } 86 | 87 | public IEnumerable SelectedTargets => 88 | TargetEntries 89 | .Where(t => t.Selected) 90 | .Select(t => t.Target); 91 | 92 | private void ElementSelected(object sender, RoutedEventArgs e) 93 | { 94 | SelectedTargetText = string.Join(", ", SelectedTargets); 95 | 96 | if (TargetSource != null) 97 | { 98 | TargetSource.Clear(); 99 | foreach (var t in SelectedTargets) 100 | { 101 | TargetSource.Add(t); 102 | } 103 | } 104 | 105 | ButtonText.Text = $"{SelectedTargetText}"; 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/TargetSelectorEntry.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | using JetBrains.Annotations; 4 | using StringsCollector.Data; 5 | 6 | namespace LocalizationTracker.Components 7 | { 8 | public class TargetSelectorEntry 9 | { 10 | private bool m_Selected; 11 | 12 | public bool Selected 13 | { 14 | get { return m_Selected; } 15 | set 16 | { 17 | { 18 | m_Selected = value; 19 | OnPropertyChanged(nameof(Selected)); 20 | } 21 | } 22 | } 23 | 24 | public Locale Target { get; } 25 | 26 | public TargetSelectorEntry(Locale target, bool selected) 27 | { 28 | Target = target; 29 | Selected = selected; 30 | } 31 | 32 | public event PropertyChangedEventHandler? PropertyChanged; 33 | 34 | [NotifyPropertyChangedInvocator] 35 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") 36 | { 37 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /LocalizationTracker/Components/TraitDetailsView.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | Trait 13 | 14 | 15 | 16 | Updated 17 | 18 | 19 | 20 | Text 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/TraitDetailsView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace LocalizationTracker.Components 17 | { 18 | /// 19 | /// Interaction logic for TraitDetailsView.xaml 20 | /// 21 | public partial class TraitDetailsView : UserControl 22 | { 23 | public TraitDetailsView() 24 | { 25 | InitializeComponent(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LocalizationTracker/Components/TraitSelectorEntry.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | using JetBrains.Annotations; 4 | 5 | namespace LocalizationTracker.Components 6 | { 7 | public class TraitSelectorEntry 8 | { 9 | private bool m_Selected; 10 | 11 | public bool Selected 12 | { 13 | get { return m_Selected; } 14 | set 15 | { 16 | { 17 | m_Selected = value; 18 | OnPropertyChanged(nameof(Selected)); 19 | } 20 | } 21 | } 22 | 23 | public string Trait { get; } 24 | 25 | public TraitSelectorEntry(string trait, bool selected) 26 | { 27 | Trait = trait; 28 | Selected = selected; 29 | } 30 | 31 | public event PropertyChangedEventHandler? PropertyChanged; 32 | 33 | [NotifyPropertyChangedInvocator] 34 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") 35 | { 36 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /LocalizationTracker/Components/TraitsSelector.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 31 | 32 | 33 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /LocalizationTracker/Data/BoundsHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LocalizationTracker.Data 4 | { 5 | public delegate int Comparison(T x, Q y); 6 | 7 | public static class BoundsHelper 8 | { 9 | public static int LowerBound(this IList array, int start, int count, Q value, Comparison comparison) 10 | { 11 | while (count > 0) 12 | { 13 | int step = count / 2; 14 | int middle = start + step; 15 | 16 | if (comparison(array[middle], value) < 0) 17 | { 18 | start = middle + 1; 19 | count -= step + 1; 20 | } 21 | else 22 | count = step; 23 | } 24 | 25 | return start; 26 | } 27 | 28 | public static int LowerBound(this IList array, int start, int count, T value) 29 | => LowerBound(array, start, count, value, Comparer.Default.Compare); 30 | 31 | public static int LowerBound(this IList array, T value) 32 | => LowerBound(array, 0, array.Count, value, Comparer.Default.Compare); 33 | 34 | public static int LowerBound(this IList array, Q value, Comparison comparison) 35 | => LowerBound(array, 0, array.Count, value, comparison); 36 | 37 | public static int UpperBound(this IList array, int start, int count, Q value, Comparison comparer) 38 | { 39 | while (count > 0) 40 | { 41 | int step = count / 2; 42 | int middle = start + step; 43 | 44 | if (comparer(array[middle], value) <= 0) 45 | { 46 | start = middle + 1; 47 | count -= step + 1; 48 | } 49 | else 50 | count = step; 51 | } 52 | return start; 53 | } 54 | 55 | public static int UpperBound(this IList array, int start, int count, T value) 56 | => UpperBound(array, start, count, value, Comparer.Default.Compare); 57 | 58 | public static int UpperBound(this IList array, T value) 59 | => UpperBound(array, 0, array.Count, value, Comparer.Default.Compare); 60 | 61 | public static int UpperBound(this IList array, Q value, Comparison comparer) 62 | => UpperBound(array, 0, array.Count, value, comparer); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /LocalizationTracker/Data/ExportRequestResult.cs: -------------------------------------------------------------------------------- 1 | using StringsCollector.Data; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LocalizationTracker.Data 7 | { 8 | public class ExportRequestResult 9 | { 10 | public ExportRequestResult(Locale source, Locale target) 11 | { 12 | SourceLocale = source; 13 | TargetLocale = target; 14 | } 15 | 16 | #region Fields 17 | 18 | LinkedList _fileNames = new LinkedList(); 19 | 20 | #endregion Fields 21 | #region Properties 22 | 23 | public Locale SourceLocale { get; } 24 | 25 | public Locale TargetLocale { get; } 26 | 27 | public int SourceVocalWordCount { get; private set; } 28 | public int SourceTotalWordCount { get; private set; } 29 | 30 | public int SourceSymbolCount { get; private set; } 31 | 32 | public IReadOnlyCollection FileNames => _fileNames; 33 | 34 | public bool IsEmpty => FileNames.Count == 0; 35 | 36 | #endregion Properties 37 | #region Methods 38 | 39 | public void Append(ExportResults result) 40 | { 41 | if (result == default) 42 | return; 43 | 44 | if (result.SourceLocale == SourceLocale && result.TargetLocale == TargetLocale) 45 | { 46 | _fileNames.AddLast(result.FileName); 47 | SourceVocalWordCount += result.SourceVocalWordCount; 48 | SourceTotalWordCount += result.SourceTotalWordCount; 49 | SourceSymbolCount += result.SourceSymbolCount; 50 | } 51 | } 52 | 53 | public string GenerateText() 54 | { 55 | var sb = new StringBuilder(); 56 | if (SourceLocale == TargetLocale) 57 | { 58 | sb.Append($"Exported for correction: {SourceLocale}\n"); 59 | } 60 | else 61 | { 62 | sb.Append($"Exported for translation: {SourceLocale} => {TargetLocale}\n"); 63 | } 64 | 65 | if (FileNames.Count == 1) 66 | sb.Append($"File: {FileNames.First()}\n"); 67 | else 68 | sb.Append($"Files: {FileNames.Count}\n"); 69 | if(TargetLocale == Locale.Sound) 70 | sb.Append($"Wordcount for VO: {SourceVocalWordCount}\n"); 71 | sb.Append($"Total Word Count: {SourceTotalWordCount}\n"); 72 | sb.Append($"Symbol Count: {SourceSymbolCount}\n"); 73 | return sb.ToString(); 74 | } 75 | 76 | #endregion Methods 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /LocalizationTracker/Data/ExportResults.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using StringsCollector.Data; 3 | using LocalizationTracker.Utility; 4 | 5 | namespace LocalizationTracker.Data 6 | { 7 | public class ExportResults 8 | { 9 | public Locale SourceLocale { get; } 10 | 11 | public Locale TargetLocale { get; } 12 | 13 | public string FileName { get; } 14 | 15 | public int SourceVocalWordCount { get; private set; } 16 | public int SourceTotalWordCount { get; private set; } 17 | 18 | public int SourceSymbolCount { get; private set; } 19 | 20 | public ExportResults(Locale sourceLocale, Locale targetLocale, string fileName) 21 | { 22 | SourceLocale = sourceLocale; 23 | TargetLocale = targetLocale; 24 | FileName = fileName; 25 | } 26 | 27 | public void AddText(string text) 28 | { 29 | SourceSymbolCount += text.Length; 30 | var wordCount = StringUtils.CountTotalWords(text); 31 | if(TargetLocale == Locale.Sound) 32 | SourceVocalWordCount += wordCount - StringUtils.CountNonVocalWords(text); 33 | SourceTotalWordCount += wordCount; 34 | } 35 | 36 | public string GenerateText() 37 | { 38 | var sb = new StringBuilder(); 39 | if (SourceLocale == TargetLocale) 40 | { 41 | sb.Append($"Exported for correction: {SourceLocale}\n"); 42 | } 43 | else 44 | { 45 | sb.Append($"Exported for translation: {SourceLocale} => {TargetLocale}\n"); 46 | } 47 | 48 | sb.Append($"File: {FileName}\n"); 49 | if (TargetLocale == Locale.Sound) 50 | sb.Append($"Wordcount for VO: {SourceVocalWordCount}\n"); 51 | sb.Append($"Total Word Count: {SourceTotalWordCount}\n"); 52 | sb.Append($"Symbols Count: {SourceSymbolCount}\n"); 53 | return sb.ToString(); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/ImportEntry.cs: -------------------------------------------------------------------------------- 1 | using LocalizationTracker.Components; 2 | using LocalizationTracker.Tools; 3 | 4 | namespace LocalizationTracker.Data 5 | { 6 | public class ImportEntry 7 | { 8 | public ImportStatus Status { get; set; } 9 | 10 | public string Messages { get; set; } 11 | 12 | public string Key { get; set; } 13 | 14 | public string Path { get; set; } 15 | 16 | public string CurrentSource { get; set; } 17 | 18 | public string ImportSource { get; set; } 19 | 20 | public InlinesWrapper SourceDiffs { get; set; } 21 | 22 | public string CurrentTarget { get; set; } 23 | 24 | public string ImportTarget { get; set; } 25 | 26 | public InlinesWrapper TargetDiffs { get; set; } 27 | 28 | public string ImportResult { get; set; } 29 | 30 | public InlinesWrapper ResultDiffs { get; set; } 31 | 32 | public ImportEntry() 33 | { 34 | Status = ImportStatus.Ok; 35 | Messages = ""; 36 | Key = ""; 37 | Path = ""; 38 | CurrentSource = ""; 39 | ImportSource = ""; 40 | SourceDiffs = new InlinesWrapper(); 41 | CurrentTarget = ""; 42 | ImportTarget = ""; 43 | TargetDiffs = new InlinesWrapper(); 44 | ImportResult = ""; 45 | ResultDiffs = new InlinesWrapper(); 46 | } 47 | 48 | public void AddMessage(string message) 49 | { 50 | if (Messages == "") 51 | { 52 | Messages = message; 53 | } 54 | else 55 | { 56 | Messages += "\n" + message; 57 | } 58 | } 59 | 60 | public void MakeDiffs() 61 | { 62 | SourceDiffs = Diff.MakeInlines(ImportSource, CurrentSource); 63 | TargetDiffs = Diff.MakeInlines(ImportTarget, CurrentTarget); 64 | ResultDiffs = Diff.MakeInlines(CurrentTarget, ImportResult); 65 | } 66 | } 67 | 68 | public enum ImportStatus 69 | { 70 | Ok, 71 | Warning, 72 | Error 73 | } 74 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/ImportFileResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LocalizationTracker.Data 4 | { 5 | public class ImportResult 6 | { 7 | public ImportResult(string filePath, string langGroup, List entries) 8 | { 9 | LanguageGroup = langGroup; 10 | _importEntries = entries; 11 | _sourceFiles = new List() { filePath }; 12 | } 13 | 14 | List _importEntries; 15 | List _sourceFiles; 16 | 17 | public readonly string LanguageGroup; 18 | public IReadOnlyList SourceFiles => _sourceFiles; 19 | public IReadOnlyList ImportEntries => _importEntries; 20 | 21 | public void UnionWith(ImportResult fileResult) 22 | { 23 | if (fileResult.LanguageGroup.Equals(LanguageGroup)) 24 | { 25 | _sourceFiles.AddRange(fileResult._sourceFiles); 26 | _importEntries.AddRange(fileResult._importEntries); 27 | } 28 | } 29 | 30 | public override string ToString() 31 | => $"{LanguageGroup}. Строк: {ImportEntries.Count}"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LocalizationTracker/Data/ImportRequestResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LocalizationTracker.Data 4 | { 5 | public class ImportRequestResult 6 | { 7 | public ImportRequestResult() 8 | { 9 | _importResults = new Dictionary(); 10 | } 11 | 12 | Dictionary _importResults; 13 | 14 | public IReadOnlyDictionary ImportResults => _importResults; 15 | 16 | public void AppendResult(ImportResult fileResult) 17 | { 18 | if (_importResults.TryGetValue(fileResult.LanguageGroup, out var existResult)) 19 | existResult.UnionWith(fileResult); 20 | else _importResults.Add(fileResult.LanguageGroup, fileResult); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LocalizationTracker/Data/JsonLocalizationData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json.Serialization; 6 | using System.Threading.Tasks; 7 | 8 | namespace LocalizationTracker.Data 9 | { 10 | public class JsonLocalizationData 11 | { 12 | [JsonInclude] 13 | [JsonPropertyName("$id")] 14 | public string Id { get; set; } 15 | 16 | [JsonInclude] 17 | [JsonPropertyName("strings")] 18 | public Dictionary Strings { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LocalizationTracker/Data/Shared/LocaleTrait.cs: -------------------------------------------------------------------------------- 1 | namespace Kingmaker.Localization.Shared 2 | { 3 | public enum LocaleTrait 4 | { 5 | CheckMe, 6 | Translated, 7 | Final 8 | } 9 | 10 | public static class LocaleTraitExtensions 11 | { 12 | public static readonly LocaleTrait[] Values = 13 | { 14 | LocaleTrait.CheckMe, 15 | LocaleTrait.Translated, 16 | LocaleTrait.Final 17 | }; 18 | } 19 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Shared/StringTrait.cs: -------------------------------------------------------------------------------- 1 | namespace Kingmaker.Localization.Shared 2 | { 3 | public enum StringTrait 4 | { 5 | Invalid, 6 | NotUsed 7 | } 8 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Shared/TraitData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using JetBrains.Annotations; 4 | 5 | namespace Kingmaker.Localization.Shared; 6 | 7 | public class TraitData:ITraitData 8 | { 9 | [NotNull] 10 | [JsonInclude] 11 | [JsonPropertyName("trait")] 12 | public readonly string Trait = ""; 13 | 14 | [JsonInclude] 15 | [JsonPropertyName("trait_date")] 16 | public DateTimeOffset ModificationDate; 17 | 18 | // locale text at the time trait was added 19 | [NotNull] 20 | [JsonInclude] 21 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 22 | [JsonPropertyName("locale_text")] 23 | public string LocaleText = ""; 24 | 25 | [JsonConstructor] 26 | public TraitData([NotNull] string trait) 27 | { 28 | Trait = trait; 29 | } 30 | 31 | string ITraitData.Trait => Trait; 32 | 33 | DateTimeOffset ITraitData.ModificationDate 34 | { 35 | get => ModificationDate; 36 | set => ModificationDate=value; 37 | } 38 | 39 | string ITraitData.LocaleText 40 | { 41 | get => LocaleText; 42 | set => LocaleText=value; 43 | } 44 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/StringEntryComparison.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace LocalizationTracker.Data 5 | { 6 | public static class StringEntryComparison 7 | { 8 | public static int LimitLen(StringEntry x, string y) 9 | { 10 | if (x == null && y == null) 11 | return 0; 12 | 13 | if (x == null) 14 | return -1; 15 | 16 | if (y == null) 17 | return 1; 18 | 19 | int len = y.Length; 20 | 21 | var dirCompareResult = string.Compare(x.DirectoryRelativeToStringsFolder, 0, y, 0, len, StringComparison.InvariantCultureIgnoreCase); 22 | if(dirCompareResult == 0) 23 | { 24 | if (x.DirectoryRelativeToStringsFolder.Length == len) 25 | return 0; 26 | return x.DirectoryRelativeToStringsFolder[len-1]=='/' ? 0 : 1; 27 | } 28 | return dirCompareResult; 29 | } 30 | 31 | public static int Exact(StringEntry x, string y) 32 | { 33 | if (x == null && y == null) 34 | return 0; 35 | 36 | if (x == null) 37 | return -1; 38 | 39 | if (y == null) 40 | return 1; 41 | 42 | var dirCompareResult = string.Compare(x.DirectoryRelativeToStringsFolder, y, StringComparison.InvariantCultureIgnoreCase); 43 | return dirCompareResult; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LocalizationTracker/Data/Unreal/UnrealLocaleData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | using Kingmaker.Localization.Shared; 5 | using LocalizationTracker.Data.Wrappers; 6 | using LocalizationTracker.Utility; 7 | 8 | namespace LocalizationTracker.Data.Unreal; 9 | 10 | public class UnrealLocaleData : ILocaleData 11 | { 12 | [JsonInclude] 13 | [JsonPropertyName("locale")] 14 | public Locale m_Locale; 15 | 16 | [JsonInclude] 17 | [JsonPropertyName("sourceLocale")] 18 | public Locale? m_TranslatedFrom; 19 | 20 | [JsonInclude] 21 | [JsonPropertyName("text")] 22 | public string m_Text; 23 | 24 | [JsonInclude] 25 | [JsonPropertyName("modification_date")] 26 | public long m_ModificationDateInTicks; 27 | 28 | [JsonInclude] 29 | [JsonPropertyName("translation_date")] 30 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 31 | [JsonDefaultValue(0)] 32 | public long m_TranslationDateInTicks; 33 | 34 | [JsonInclude] 35 | [JsonPropertyName("original_text")] 36 | public string m_OriginalText; 37 | 38 | [JsonInclude] 39 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault), JsonDefaultValue("")] 40 | [JsonPropertyName("translation_comment")] 41 | public string m_Comment = ""; 42 | 43 | [JsonInclude] 44 | [JsonPropertyName("traits")] 45 | public List m_Traits = new(); 46 | 47 | [JsonIgnore] 48 | public Locale Locale => m_Locale; 49 | 50 | [JsonIgnore] 51 | public string Text 52 | { 53 | get => m_Text; 54 | set => m_Text = value; 55 | } 56 | 57 | [JsonIgnore] 58 | public DateTimeOffset ModificationDate 59 | { 60 | get => m_ModificationDateInTicks > 0 61 | ? DateTime.FromFileTimeUtc(m_ModificationDateInTicks - UnrealStringData.FileTimeOffset) 62 | : DateTimeOffset.MinValue; 63 | set => m_ModificationDateInTicks = value.UtcDateTime.ToFileTimeUtc() + UnrealStringData.FileTimeOffset; 64 | } 65 | 66 | [JsonIgnore] 67 | public Locale? TranslatedFrom 68 | { 69 | get => m_TranslatedFrom; 70 | set => m_TranslatedFrom = value; 71 | } 72 | 73 | [JsonIgnore] 74 | public DateTimeOffset? TranslationDate 75 | { 76 | get => m_TranslationDateInTicks > 0 77 | ? DateTime.FromFileTimeUtc(m_TranslationDateInTicks - UnrealStringData.FileTimeOffset) 78 | : null; 79 | set => m_TranslationDateInTicks = value?.UtcDateTime.ToFileTimeUtc() + UnrealStringData.FileTimeOffset ?? 0; 80 | } 81 | 82 | [JsonIgnore] 83 | public string OriginalText 84 | { 85 | get => m_OriginalText; 86 | set => m_OriginalText = value; 87 | } 88 | 89 | [JsonIgnore] 90 | public string TranslatedComment 91 | { 92 | get => m_Comment; 93 | set => m_Comment = value; 94 | } 95 | 96 | [JsonIgnore] 97 | public IEnumerable Traits => m_Traits ??= new List(); 98 | 99 | public UnrealLocaleData(Locale locale) 100 | { 101 | m_Locale = locale; 102 | m_ModificationDateInTicks = DateTime.UtcNow.ToFileTimeUtc(); 103 | } 104 | public void AddTraitInternal(ITraitData trait) 105 | { 106 | m_Traits ??= new List(); 107 | m_Traits.Add((UnrealTraitData)trait); 108 | } 109 | 110 | public void RemoveTraitInternal(string trait) 111 | { 112 | m_Traits?.RemoveAll(t => t.Trait == trait); 113 | } 114 | [JsonConstructor] 115 | public UnrealLocaleData() 116 | { 117 | } 118 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Unreal/UnrealTraitData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using Kingmaker.Localization.Shared; 4 | 5 | namespace LocalizationTracker.Data.Unreal; 6 | 7 | public class UnrealTraitData:ITraitData 8 | { 9 | [JsonInclude] 10 | [JsonPropertyName("trait")] 11 | public readonly string Trait = ""; 12 | 13 | [JsonInclude] 14 | [JsonPropertyName("trait_date")] 15 | public long m_ModificationDateInTicks; 16 | 17 | // locale text at the time trait was added 18 | [JsonInclude] 19 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 20 | [JsonPropertyName("locale_text")] 21 | public string LocaleText = ""; 22 | 23 | [JsonConstructor] 24 | public UnrealTraitData(string trait) 25 | { 26 | Trait = trait; 27 | } 28 | 29 | string ITraitData.Trait => Trait; 30 | 31 | [JsonIgnore] 32 | public DateTimeOffset ModificationDate 33 | { 34 | get => m_ModificationDateInTicks > 0 35 | ? DateTime.FromFileTimeUtc(m_ModificationDateInTicks - UnrealStringData.FileTimeOffset) 36 | : DateTimeOffset.MinValue; 37 | set => m_ModificationDateInTicks = value.UtcDateTime.ToFileTimeUtc() + UnrealStringData.FileTimeOffset; 38 | } 39 | 40 | string ITraitData.LocaleText 41 | { 42 | get => LocaleText; 43 | set => LocaleText=value; 44 | } 45 | 46 | // virtual traits get created when string is loaded and are never saved 47 | public bool IsVirtual { get; set; } 48 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Wrappers/ILocaleData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Kingmaker.Localization.Shared; 5 | 6 | namespace LocalizationTracker.Data.Wrappers; 7 | 8 | public interface ILocaleData 9 | { 10 | public Locale Locale { get; } 11 | 12 | public string Text { get; } 13 | 14 | public DateTimeOffset ModificationDate { get; } 15 | 16 | public Locale? TranslatedFrom { get; } 17 | 18 | public DateTimeOffset? TranslationDate { get; } 19 | 20 | public string OriginalText { get; set; } 21 | 22 | public string TranslatedComment { get; set; } 23 | 24 | public IEnumerable Traits { get; } 25 | 26 | public bool HasTrait(string trait) 27 | { 28 | return Traits != null && Traits.Any(t => t.Trait == trait); 29 | } 30 | 31 | void AddTraitInternal(ITraitData trait); 32 | void RemoveTraitInternal(string trait); 33 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Wrappers/IStringData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | using Kingmaker.Localization.Shared; 6 | using LocalizationTracker.Data.Unreal; 7 | using static LocalizationTracker.Data.Unreal.UnrealStringData; 8 | 9 | namespace LocalizationTracker.Data.Wrappers; 10 | 11 | public interface IStringData 12 | { 13 | public Locale Source { get; } 14 | 15 | public string Key { get; } 16 | 17 | public string Comment { get; set; } 18 | 19 | public StringKind Kind { get; } 20 | 21 | public string Speaker { get; } 22 | 23 | public string SpeakerGender { get; } 24 | 25 | public ParentId ParentId { get; } 26 | 27 | public string OwnerLink { get; } 28 | public string StringPath { get; } 29 | public DateTimeOffset ModificationDate { get; } 30 | 31 | public string AttachmentPath { get; } 32 | 33 | 34 | public IEnumerable Languages { get; } 35 | 36 | public IEnumerable StringTraits { get; } 37 | bool ShouldCount { get; } 38 | 39 | bool UpdateText(Locale locale, string text, bool updateDate = true); 40 | void UpdateTranslation(Locale locale, string text, Locale translatedFrom, string originalText); 41 | 42 | void AddTraitInternal(ITraitData trait); 43 | void RemoveTraitInternal(string trait); 44 | 45 | public void AddTrait(Locale locale, string trait) 46 | { 47 | var localeData = GetLocale(locale); 48 | if (localeData == null) 49 | return; 50 | 51 | var traitData = localeData.Traits.FirstOrDefault(t => t.Trait == trait); 52 | if (traitData == null) 53 | { 54 | traitData = CreateTraitData(trait); 55 | localeData.AddTraitInternal(traitData); 56 | } 57 | 58 | traitData.LocaleText = localeData.Text; 59 | traitData.ModificationDate = DateTimeOffset.UtcNow; 60 | } 61 | 62 | ITraitData CreateTraitData(string trait, bool isVirtual=false); 63 | 64 | public void RemoveTrait(Locale locale, string trait) 65 | { 66 | var localeData = GetLocale(locale); 67 | localeData?.RemoveTraitInternal(trait); 68 | } 69 | 70 | public void AddStringTrait(string trait, bool isVirtual = false) 71 | { 72 | var traitData = StringTraits.FirstOrDefault(t => t.Trait == trait); 73 | if (traitData == null) 74 | { 75 | traitData = CreateTraitData(trait, isVirtual); 76 | AddTraitInternal(traitData); 77 | } 78 | 79 | traitData.ModificationDate = DateTimeOffset.UtcNow; 80 | } 81 | 82 | public void RemoveStringTrait(string trait) 83 | { 84 | RemoveTraitInternal(trait); 85 | } 86 | 87 | public bool HasStringTrait(string trait) 88 | { 89 | return StringTraits != null && StringTraits.Any(t => t.Trait == trait); 90 | } 91 | public string[] GetStringTraits() 92 | { 93 | if (StringTraits == null) 94 | return new string[] { }; 95 | 96 | return StringTraits 97 | .Select(t => t.Trait) 98 | .ToArray(); 99 | } 100 | 101 | public ILocaleData? GetLocale(Locale locale) => Languages.FirstOrDefault(s => s.Locale == locale); 102 | 103 | public ILocaleData EnsureLocale(Locale locale); 104 | 105 | public ITraitData? GetTraitData(Locale locale, string trait) 106 | { 107 | var localeData = GetLocale(locale); 108 | return localeData?.Traits?.FirstOrDefault(td => td.Trait == trait); 109 | } 110 | 111 | public ITraitData? GetStringTraitData(string trait) 112 | { 113 | return StringTraits?.FirstOrDefault(td => td.Trait == trait); 114 | } 115 | 116 | public string GetText(Locale locale) 117 | { 118 | var localeData = GetLocale(locale); 119 | return localeData?.Text ?? ""; 120 | } 121 | 122 | public string[] GetTraits(Locale locale) 123 | { 124 | var localeData = GetLocale(locale); 125 | if (localeData == null) 126 | return new string[] { }; 127 | 128 | if (localeData.Traits == null) 129 | return new string[] { }; 130 | 131 | return localeData.Traits 132 | .Select(t => t.Trait) 133 | .ToArray(); 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Wrappers/ITraitData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Kingmaker.Localization.Shared; 4 | 5 | public interface ITraitData 6 | { 7 | string Trait { get; } 8 | 9 | DateTimeOffset ModificationDate { get; set; } 10 | 11 | // locale text at the time trait was added 12 | string LocaleText { get; set; } 13 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Wrappers/StringsArchive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Xml.Linq; 7 | using LocalizationTracker.Logic; 8 | 9 | namespace LocalizationTracker.Data.Wrappers; 10 | 11 | /// 12 | /// Wraps the whole set of strings on disk. For Unity this is a folder with string files, for Unreal this is one giant json 13 | /// 14 | public abstract class StringsArchive 15 | { 16 | public abstract List LoadAll( 17 | DirectoryInfo rootDir, StringEntry[]? skipUnmodified, CancellationToken ct); 18 | public abstract Task SaveAll(); 19 | public abstract void Save(IStringData str); 20 | public abstract IStringData Reload(IStringData str); 21 | public abstract void Delete(IStringData str); 22 | public virtual bool HasUnsavedChanges => false; // Unity can save any string immediately, but Unreal has to track changes and save the whole list 23 | 24 | public virtual bool IsFileModified(IStringData str) => false; 25 | 26 | public event Action StringChanged= () => { }; 27 | 28 | protected void RaiseStringChanged() 29 | { 30 | StringChanged(); 31 | } 32 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Wrappers/StringsArchiveUnity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.Json; 7 | using System.Text.RegularExpressions; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Kingmaker.Localization.Shared; 11 | using LocalizationTracker.Utility; 12 | 13 | namespace LocalizationTracker.Data.Wrappers; 14 | 15 | partial class StringsArchiveUnity : StringsArchive 16 | { 17 | public override List LoadAll(DirectoryInfo rootDir, StringEntry[]? skipUnmodified, CancellationToken ct) 18 | { 19 | HashSet files = rootDir.EnumerateFiles("*.json", SearchOption.AllDirectories) 20 | .AsParallel() 21 | .WithCancellation(ct) 22 | .Select(f => f.FullName) 23 | .ToHashSet(); 24 | 25 | if (skipUnmodified!=null) 26 | { 27 | foreach (var se in skipUnmodified) 28 | files.Remove(se.AbsolutePath); 29 | } 30 | 31 | var newLoadedEntries = files 32 | .AsParallel() 33 | .WithCancellation(ct) 34 | .Select(LoadData); 35 | 36 | return newLoadedEntries.ToList(); 37 | } 38 | 39 | private IStringData LoadData(string absolutePath) 40 | { 41 | if (absolutePath.Length >= 270) 42 | { 43 | throw new IOException($"The path is too long: {absolutePath}"); 44 | } 45 | 46 | LocalizedStringData? Data = null; 47 | 48 | var modificationTime = File.GetLastWriteTime(absolutePath); 49 | 50 | var rootUri = new Uri(Path.GetFullPath(AppConfig.Instance.StringsFolder), UriKind.Absolute); 51 | var fileUri = new Uri(absolutePath, UriKind.Absolute); 52 | 53 | try 54 | { 55 | using (var sr = new StreamReader(absolutePath)) 56 | { 57 | var text = sr.ReadToEnd(); 58 | Data = JsonSerializer.Deserialize(text, JsonSerializerHelpers.JsonSerializerOptions); 59 | } 60 | } 61 | catch (Exception e) 62 | { 63 | throw new IOException($"Failed to parse json file: {absolutePath}", e); 64 | } 65 | 66 | if (Data == null) 67 | throw new IOException($"Failed to parse json file (data is null): {absolutePath}"); 68 | 69 | Data.AbsolutePath = absolutePath; 70 | Data.StringPath = rootUri.MakeRelativeUri(fileUri).ToString(); 71 | Data.ModificationDate = modificationTime; 72 | 73 | return Data; 74 | } 75 | 76 | public override Task SaveAll() 77 | { 78 | throw new System.NotImplementedException(); 79 | } 80 | 81 | public override void Save(IStringData str) 82 | { 83 | var absolutePath = ((LocalizedStringData)str).AbsolutePath; 84 | if (!File.Exists(absolutePath)) 85 | { 86 | throw new Exception("Строка больше не существует по старому пути. Нажмите Rescan или перезапустите локтулзу прежде, чем продолжить"); 87 | } 88 | 89 | using (var sw = new StreamWriter(absolutePath)) 90 | { 91 | var json = JsonSerializer.Serialize((LocalizedStringData)str, JsonSerializerHelpers.JsonSerializerOptions); 92 | var unescaped = JsonSerializerHelpers.UnescapeUnicodeSymbols(json); 93 | sw.Write(unescaped); 94 | } 95 | } 96 | 97 | public override IStringData Reload(IStringData str) 98 | { 99 | var absolutePath = ((LocalizedStringData)str).AbsolutePath; 100 | if (!File.Exists(absolutePath)) 101 | { 102 | throw new Exception("Строка больше не существует по старому пути. Нажмите Rescan или перезапустите локтулзу прежде, чем продолжить"); 103 | } 104 | 105 | return LoadData(absolutePath); 106 | } 107 | 108 | public override void Delete(IStringData str) 109 | { 110 | File.Delete(((LocalizedStringData)str).AbsolutePath); 111 | } 112 | 113 | public override bool IsFileModified(IStringData str) 114 | { 115 | var absolutePath = ((LocalizedStringData)str).AbsolutePath; 116 | return !File.Exists(absolutePath) || File.GetLastWriteTime(absolutePath) > str.ModificationDate; 117 | } 118 | } -------------------------------------------------------------------------------- /LocalizationTracker/Data/Wrappers/TextFixupHelper.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Localization.Shared; 2 | 3 | namespace LocalizationTracker.Data.Wrappers; 4 | 5 | public class TextFixupHelper 6 | { 7 | public static string ApplyFixups(Locale locale, string text) 8 | { 9 | if (locale == Locale.ruRU || locale == Locale.enGB) // todo: this needs some sort of config 10 | { 11 | text = text.Replace("«", "\""); 12 | text = text.Replace("»", "\""); 13 | text = text.Replace("“", "\""); 14 | text = text.Replace("”", "\""); 15 | text = text.Replace("’", "'"); 16 | } 17 | 18 | text = text.Replace(" - ", " — "); 19 | text = text.Replace("\r", ""); 20 | //text = text.TrimEnd(); 21 | while (text.Contains(" ")) 22 | { 23 | text = text.Replace(" ", " "); 24 | } 25 | while (text.Contains(" \n")) 26 | { 27 | text = text.Replace(" \n", "\n"); 28 | } 29 | while (text.Contains("\n ")) 30 | { 31 | text = text.Replace("\n ", "\n"); 32 | } 33 | while (text.Contains("\n\n")) 34 | { 35 | text = text.Replace("\n\n", "\n"); 36 | } 37 | 38 | return text; 39 | } 40 | } -------------------------------------------------------------------------------- /LocalizationTracker/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using StringsCollector; 2 | global using StringsCollector.Data; 3 | global using StringsCollector.Utility; 4 | 5 | -------------------------------------------------------------------------------- /LocalizationTracker/LocalizationTracker.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0-windows 4 | WinExe 5 | 1 6 | 1.0.0.%2a 7 | true 8 | true 9 | embedded 10 | win-x64 11 | true 12 | 13 | 14 | FolderProfile 15 | enable 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 | SettingsSingleFileGenerator 42 | 43 | 44 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/ExcelExctentions.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml.Packaging; 2 | using DocumentFormat.OpenXml.Spreadsheet; 3 | using System.Linq; 4 | 5 | namespace LocalizationTracker.Excel 6 | { 7 | public static class ExcelExctentions 8 | { 9 | public static SharedStringTablePart GetSharedTable(this SpreadsheetDocument spreadSheet) 10 | { 11 | SharedStringTablePart sharedStringPart; 12 | if (spreadSheet.WorkbookPart.GetPartsOfType().Count() > 0) 13 | { 14 | sharedStringPart = spreadSheet.WorkbookPart.GetPartsOfType().First(); 15 | } 16 | else 17 | { 18 | sharedStringPart = spreadSheet.WorkbookPart.AddNewPart(); 19 | sharedStringPart.SharedStringTable = new SharedStringTable(); 20 | } 21 | 22 | return sharedStringPart; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/ExcelStyles.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml; 2 | using DocumentFormat.OpenXml.Spreadsheet; 3 | using LocalizationTracker.Components; 4 | using LocalizationTracker.Logic.Excel; 5 | using System.Windows.Media; 6 | 7 | namespace LocalizationTracker.Excel 8 | { 9 | public class ExcelStyles: IExcelStyler 10 | { 11 | public virtual SharedStringItem DiffToSharedString(InlinesWrapper inlineWrapper) 12 | { 13 | SharedStringItem item = new SharedStringItem(); 14 | foreach (var inline in inlineWrapper.InlineTemplates) 15 | { 16 | var run = inline; 17 | var xmlRun = new Run(); 18 | if (!CheckRedStrike(run, xmlRun)) 19 | CheckColor(run, xmlRun); 20 | 21 | var text = new Text() { Space = SpaceProcessingModeValues.Preserve }; 22 | text.Text = run.Text; 23 | xmlRun.Append(text); 24 | item.Append(xmlRun); 25 | } 26 | 27 | return item; 28 | } 29 | 30 | protected virtual bool CheckRedStrike(InlineTemplate baseRun, Run xmlRun) 31 | { 32 | if (baseRun.Background == ColorUtility.Red) 33 | { 34 | xmlRun.Append(GetRedStriked()); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public virtual void CheckColor(InlineTemplate baseRun, Run xmlRun) 42 | { 43 | if (baseRun.Background != null) 44 | { 45 | var prop = GetColorProp(baseRun.Background == ColorUtility.Green ? ColorUtility.ExportDiffGreen : baseRun.Background.Value); 46 | xmlRun.Append(prop); 47 | } 48 | } 49 | 50 | protected virtual RunProperties GetRedStriked() 51 | { 52 | RunProperties runProperties1 = new RunProperties(); 53 | var strike = new Strike(); 54 | var color = ColorUtility.MediaColorToOXMLColor(ColorUtility.ExportDiffRed); 55 | runProperties1.Append(strike); 56 | runProperties1.Append(color); 57 | 58 | return runProperties1; 59 | } 60 | 61 | public virtual RunProperties GetColorProp(System.Windows.Media.Color color) 62 | { 63 | var colorProp = new RunProperties(); 64 | var redColor = ColorUtility.MediaColorToOXMLColor(color); 65 | colorProp.Append(redColor); 66 | 67 | return colorProp; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/ExcelTagMismatchStyle.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml; 2 | using DocumentFormat.OpenXml.Spreadsheet; 3 | using LocalizationTracker.Components; 4 | using LocalizationTracker.Excel; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace LocalizationTracker.Logic.Excel 12 | { 13 | public class ExcelTagMismatchStyle : ExcelStyles 14 | { 15 | public override SharedStringItem DiffToSharedString(InlinesWrapper inlineWrapper) 16 | { 17 | SharedStringItem item = new SharedStringItem(); 18 | foreach (var inline in inlineWrapper.InlineTemplates) 19 | { 20 | var run = inline; 21 | var xmlRun = new Run(); 22 | 23 | CheckColor(run, xmlRun); 24 | 25 | var text = new Text() { Space = SpaceProcessingModeValues.Preserve }; 26 | text.Text = run.Text; 27 | xmlRun.Append(text); 28 | item.Append(xmlRun); 29 | } 30 | 31 | return item; 32 | } 33 | 34 | public override void CheckColor(InlineTemplate baseRun, Run xmlRun) 35 | { 36 | if (baseRun.Foreground != null && baseRun.Background == null) 37 | { 38 | var foreProp = GetColorProp(baseRun.Foreground == ColorUtility.Green ? ColorUtility.ExportDiffGreen : baseRun.Foreground.Value); 39 | xmlRun.Append(foreProp); 40 | } 41 | 42 | if (baseRun.Background != null) 43 | { 44 | var backProp = GetColorProp(System.Windows.Media.Colors.Orange); 45 | xmlRun.Append(backProp); 46 | } 47 | } 48 | 49 | public override RunProperties GetColorProp(System.Windows.Media.Color color) 50 | { 51 | var colorProp = new RunProperties(); 52 | var redColor = ColorUtility.MediaColorToOXMLColor(color); 53 | 54 | colorProp.Append(redColor); 55 | 56 | if (color == System.Windows.Media.Colors.Red || color == System.Windows.Media.Colors.Orange) 57 | { 58 | colorProp.Append(new Bold()); 59 | } 60 | 61 | return colorProp; 62 | } 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/ExportWrapper.cs: -------------------------------------------------------------------------------- 1 | using LocalizationTracker.Data; 2 | using LocalizationTracker.Windows; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace LocalizationTracker.Logic.Excel 7 | { 8 | public abstract class ExportWrapper 9 | { 10 | public ExportWrapper(IExporter exporter, ExportData data) 11 | { 12 | ExportResult = new ExportRequestResult(data.ExportParams.Source, string.Join(",", data.ExportParams.Target)); 13 | _exporter = exporter; 14 | _data = data; 15 | } 16 | 17 | public ExportData _data; 18 | 19 | protected IExporter _exporter; 20 | 21 | protected IProgress _progressReporter; 22 | 23 | public ExportRequestResult ExportResult { get; private set; } 24 | 25 | public void Export(IProgress progress) 26 | { 27 | _progressReporter = progress; 28 | ExportInternal(); 29 | } 30 | 31 | protected abstract void ExportInternal(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/Exporters/ExcelSourceUpdateExporter.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml.Packaging; 2 | using DocumentFormat.OpenXml.Spreadsheet; 3 | using StringsCollector.Data; 4 | using LocalizationTracker.Components; 5 | using LocalizationTracker.Data; 6 | using LocalizationTracker.Utility; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text; 10 | namespace LocalizationTracker.Excel 11 | { 12 | public class ExcelSourceUpdateExporter : ExcelLocalizationExporter 13 | { 14 | ExcelStyles _style; 15 | 16 | public ExcelSourceUpdateExporter(ExcelStyles excelStyles) 17 | { 18 | _style = excelStyles; 19 | } 20 | public override ExportData PrepareDataToExport(ExportData data) 21 | { 22 | var prevSrcLocale = StringEntry.SourceLocale; 23 | var prevTrgLocale = StringEntry.TargetLocale; 24 | 25 | StringEntry.SourceLocale = Locale.TranslationSource; 26 | StringEntry.TargetLocale = data.ExportParams.Target.FirstOrDefault(); 27 | 28 | Dictionary pairs = new Dictionary(data.Items.Length); 29 | var builder = new StringBuilder(); 30 | foreach (var line in data.Items) 31 | { 32 | line.UpdateInlines(); 33 | if (data.ExportParams.TagRemovalPolicy == TagRemovalPolicy.DeleteUpdatedTag) 34 | { 35 | System.Windows.Media.Color color = new System.Windows.Media.Color 36 | { 37 | R = 215, 38 | G = 227, 39 | B = 188, 40 | A = 255 41 | }; 42 | 43 | var inlines = line.SourceLocaleEntry.Inlines.InlineTemplates.ToList(); 44 | 45 | for (int i = 0; i < inlines.Count; i++) 46 | { 47 | if (inlines[i].Background == color || inlines[i].StrikeThrough == true) 48 | { 49 | var text = StringUtils.RemoveTags(inlines[i].Text, LocalizationExporter.TagsEncyclopedia); 50 | line.SourceLocaleEntry.Inlines.InlineTemplates[i].Text = text; 51 | } 52 | } 53 | 54 | } 55 | 56 | var source = _style.DiffToSharedString(line.SourceLocaleEntry.Inlines); 57 | var target = _style.DiffToSharedString(line.TargetLocaleEntry.Inlines); 58 | builder.Clear(); 59 | AddClearText(builder, line.SourceLocaleEntry.Inlines); 60 | pairs.Add(line, new DiffPair(source, target, builder.ToString())); 61 | } 62 | 63 | StringEntry.SourceLocale = prevSrcLocale; 64 | StringEntry.TargetLocale = prevTrgLocale; 65 | 66 | return new DiffExportData(data, pairs); 67 | } 68 | 69 | void AddClearText(StringBuilder builder, InlinesWrapper inlineWrapper) 70 | { 71 | foreach (var inline in inlineWrapper.InlineTemplates) 72 | { 73 | builder.Append(inline.Text); 74 | } 75 | } 76 | 77 | protected override void AddTextCells(in ExportData data, SpreadsheetDocument doc, Row row, StringEntry s, ExportResults result) 78 | { 79 | if (data is DiffExportData exportData && exportData.DiffPairs.TryGetValue(s, out var pair)) 80 | { 81 | row.Append(new Cell() 82 | .SetSharedText(doc.AddSharedString(pair.SourceLocale)) 83 | .SetStyle(CellStyle.WordWrap)); 84 | row.Append(new Cell() 85 | .SetSharedText(doc.AddSharedString(pair.TargetLocaleEntry)) 86 | .SetStyle(CellStyle.WordWrap) 87 | ); 88 | 89 | result.AddText(pair.SoruceText); 90 | } 91 | } 92 | } 93 | 94 | public class DiffExportData : ExportData 95 | { 96 | public DiffExportData(ExportData data, Dictionary diffPairs) 97 | { 98 | DistFolder = data.DistFolder; 99 | Items = data.Items; 100 | ExportParams = data.ExportParams; 101 | DiffPairs = diffPairs; 102 | } 103 | 104 | public Dictionary DiffPairs; 105 | } 106 | 107 | public class DiffPair 108 | { 109 | public DiffPair(SharedStringItem source, SharedStringItem target, string sourceText) 110 | { 111 | SourceLocale = source; 112 | TargetLocaleEntry = target; 113 | SoruceText = sourceText; 114 | } 115 | 116 | public string SoruceText { get; } 117 | public SharedStringItem SourceLocale { get; } 118 | public SharedStringItem TargetLocaleEntry { get; } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/Exporters/ExcelTagsMismathExporter.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml.Packaging; 2 | using DocumentFormat.OpenXml.Spreadsheet; 3 | using LocalizationTracker.Components; 4 | using LocalizationTracker.Data; 5 | using LocalizationTracker.Excel; 6 | using LocalizationTracker.Utility; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace LocalizationTracker.Logic.Excel.Exporters 14 | { 15 | public class ExcelTagsMismathExporter : ExcelLocalizationExporter 16 | { 17 | ExcelTagMismatchStyle _style; 18 | public ExcelTagsMismathExporter(ExcelTagMismatchStyle excelStyle) 19 | { 20 | _style = excelStyle; 21 | } 22 | 23 | public override ExportData PrepareDataToExport(ExportData data) 24 | { 25 | foreach (var line in data.Items) 26 | { 27 | TagsList.Compare(line.SourceLocaleEntry.TagsList, line.TargetLocaleEntry.TagsList); 28 | line.UpdateInlines(); 29 | 30 | } 31 | 32 | return data; 33 | } 34 | 35 | protected override void AddTextCells(in ExportData data, SpreadsheetDocument doc, Row row, StringEntry entry, ExportResults result) 36 | { 37 | if (entry.SourceLocaleEntry.Inlines != null && entry.TargetLocaleEntry.Inlines != null) 38 | { 39 | var sourceSharedString = _style.DiffToSharedString(entry.SourceLocaleEntry.Inlines); 40 | var targetSharedString = _style.DiffToSharedString(entry.TargetLocaleEntry.Inlines); 41 | 42 | row.Append(new Cell() 43 | .SetSharedText(doc.AddSharedString(sourceSharedString)) 44 | .SetStyle(LocalizationTracker.Excel.CellStyle.WordWrap)); 45 | 46 | row.Append(new Cell() 47 | .SetSharedText(doc.AddSharedString(targetSharedString)) 48 | .SetStyle(LocalizationTracker.Excel.CellStyle.WordWrap)); 49 | 50 | StringBuilder builder = new StringBuilder(); 51 | AddClearText(builder, entry.SourceLocaleEntry.Inlines); 52 | result.AddText(builder.ToString()); 53 | } 54 | } 55 | 56 | private void AddClearText(StringBuilder builder, InlinesWrapper inlineWrapper) 57 | { 58 | foreach (var inline in inlineWrapper.InlineTemplates) 59 | { 60 | builder.Append(inline.Text); 61 | } 62 | } 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/IExcelStyler.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml.Spreadsheet; 2 | using LocalizationTracker.Components; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace LocalizationTracker.Logic.Excel 10 | { 11 | public interface IExcelStyler 12 | { 13 | public SharedStringItem DiffToSharedString(InlinesWrapper inlineWrapper); 14 | protected void CheckColor(InlineTemplate baseRun, Run xmlRun); 15 | protected RunProperties GetColorProp(System.Windows.Media.Color color); 16 | 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/SheetDataExtentions.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml; 2 | using DocumentFormat.OpenXml.Packaging; 3 | using DocumentFormat.OpenXml.Spreadsheet; 4 | using System.Linq; 5 | 6 | namespace LocalizationTracker.Excel 7 | { 8 | public static class SheetDataExtentions 9 | { 10 | public static Cell CreateCellWithSharedText(this Row row, string cellRef, int sharedTextIndex) 11 | { 12 | Cell cell = CreateOrGetCellInRow(cellRef, row); 13 | cell.CellValue = new CellValue(sharedTextIndex.ToString()); 14 | cell.DataType = new EnumValue(CellValues.SharedString); 15 | 16 | return cell; 17 | } 18 | 19 | public static Cell CreateCellWithText(this Row row, string cellRef, string text) 20 | { 21 | Cell cell = CreateOrGetCellInRow(cellRef, row); 22 | cell.CellValue = new CellValue(text); 23 | cell.DataType = new EnumValue(CellValues.SharedString); 24 | 25 | return cell; 26 | } 27 | 28 | public static Cell CreateEmptyCell(this Row row, string cellRef) 29 | { 30 | Cell cell = CreateOrGetCellInRow(cellRef, row); 31 | return cell; 32 | } 33 | 34 | public static Row CreateOrGetRow(this SheetData sheetData, uint rowIndex) 35 | { 36 | /*if (!sheetData.Elements().TryFind(c => c.RowIndex.Value == rowIndex, out var row)) 37 | { 38 | row = new Row() { RowIndex = rowIndex }; 39 | sheetData.Append(row); 40 | }*/ 41 | 42 | var row = new Row() { RowIndex = rowIndex }; 43 | sheetData.Append(row); 44 | return row; 45 | } 46 | 47 | static Cell CreateOrGetCellInRow(string cellReference, Row row) 48 | { 49 | /*Cell refCell = null; 50 | foreach (var cell in row.Elements()) 51 | { 52 | if (cell.CellReference.Value == cellReference) 53 | return cell; 54 | if (string.Compare(cell.CellReference.Value, cellReference, true) > 0) 55 | { 56 | refCell = cell; 57 | break; 58 | } 59 | } 60 | 61 | var newCell = new Cell() { CellReference = cellReference }; 62 | row.InsertBefore(newCell, refCell);*/ 63 | 64 | var newCell = new Cell() { CellReference = cellReference }; 65 | row.InsertBefore(newCell, null); 66 | return newCell; 67 | } 68 | 69 | public static int NextRowID = 0; 70 | 71 | public static int GetIDForSharedText(this SharedStringTablePart shareStringPart, string text) 72 | { 73 | if (shareStringPart.SharedStringTable == null) 74 | { 75 | shareStringPart.SharedStringTable = new SharedStringTable(); 76 | } 77 | 78 | //int count = shareStringPart.SharedStringTable.Elements().Count(); 79 | shareStringPart.SharedStringTable.AppendChild(new SharedStringItem(new Text(text))); 80 | return NextRowID++; 81 | //return count; 82 | 83 | //int i = 0; 84 | //foreach (SharedStringItem item in shareStringPart.SharedStringTable.Elements()) 85 | //{ 86 | // if (item.InnerText == text) 87 | // { 88 | // return i; 89 | // } 90 | 91 | // i++; 92 | //} 93 | 94 | //shareStringPart.SharedStringTable.AppendChild(new SharedStringItem(new Text(text))); 95 | //return i; 96 | } 97 | 98 | public static int GetIDForSharedText(this SharedStringTablePart shareStringPart, SharedStringItem textItem) 99 | { 100 | if (shareStringPart.SharedStringTable == null) 101 | { 102 | shareStringPart.SharedStringTable = new SharedStringTable(); 103 | } 104 | 105 | //int count = shareStringPart.SharedStringTable.Elements().Count(); 106 | shareStringPart.SharedStringTable.AppendChild(textItem); 107 | return NextRowID++; 108 | //return count; 109 | 110 | //int i = 0; 111 | //foreach (SharedStringItem item in shareStringPart.SharedStringTable.Elements()) 112 | //{ 113 | // if (item.InnerText == textItem.InnerText) 114 | // { 115 | // return i; 116 | // } 117 | 118 | // i++; 119 | //} 120 | 121 | //shareStringPart.SharedStringTable.AppendChild(textItem); 122 | //return i; 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/SheetWrapper.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml; 2 | using DocumentFormat.OpenXml.Packaging; 3 | using DocumentFormat.OpenXml.Spreadsheet; 4 | using System; 5 | 6 | namespace LocalizationTracker.Excel 7 | { 8 | class SheetWrapper 9 | { 10 | public SheetWrapper(SheetData sheet, SharedStringTablePart sharedStrings) 11 | { 12 | _worksheet = sheet; 13 | _sharedStrings = sharedStrings; 14 | IsReadyToWork = _worksheet != null && _sharedStrings != null; 15 | } 16 | 17 | #region Fields 18 | 19 | private readonly SheetData _worksheet; 20 | 21 | private readonly SharedStringTablePart _sharedStrings; 22 | 23 | uint _rowIndex = 0; 24 | 25 | int _columnIndex = 0; 26 | 27 | Row _currentRow; 28 | 29 | #endregion Fields 30 | #region Properties 31 | 32 | public bool IsReadyToWork { get; } 33 | 34 | public OpenXmlElementList ChildElements => _worksheet.ChildElements; 35 | 36 | #endregion Properties 37 | #region Methods 38 | 39 | public void NewRow() 40 | { 41 | _rowIndex++; 42 | _columnIndex = 0; 43 | _currentRow = _worksheet.CreateOrGetRow(_rowIndex); 44 | } 45 | 46 | public void AddCell(string text, CellStyle cellStyle) 47 | { 48 | var refID = _sharedStrings.GetIDForSharedText(text); 49 | AddCell(refID, cellStyle); 50 | } 51 | 52 | public void AddCell(SharedStringItem stringItem, CellStyle cellStyle) 53 | { 54 | var refID = _sharedStrings.GetIDForSharedText(stringItem); 55 | AddCell(refID, cellStyle); 56 | } 57 | 58 | void AddCell(int refID, CellStyle cellStyle) 59 | { 60 | var cellRef = GetCellRef(); 61 | var cell = _currentRow.CreateCellWithSharedText(cellRef, refID); 62 | cell.StyleIndex = new UInt32Value((uint)cellStyle); 63 | } 64 | 65 | string GetCellRef() 66 | { 67 | if (_currentRow == null) 68 | throw new Exception("Попытка вставить ячейку в несуществующий столбец"); 69 | 70 | return SpreadsheetHelper.GetCellRef(_rowIndex, _columnIndex++); 71 | } 72 | 73 | #endregion Methods 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/WorkbookWrapper.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml; 2 | using DocumentFormat.OpenXml.Packaging; 3 | using DocumentFormat.OpenXml.Spreadsheet; 4 | using LocalizationTracker.Utility; 5 | using System; 6 | using System.Linq; 7 | 8 | namespace LocalizationTracker.Excel 9 | { 10 | public class WorkbookWrapper : IDisposable 11 | { 12 | public WorkbookWrapper(string path, WrapperMod wrapperMod, ColumnSettings[]? columnDatas = null) 13 | { 14 | _path = path; 15 | _sheet = SpreadsheetHelper.GetSpreadsheet(path, wrapperMod, columnDatas); 16 | _wrapperMod = wrapperMod; 17 | if (_sheet != null && !string.IsNullOrEmpty(_path)) 18 | { 19 | try 20 | { 21 | SharedString = _sheet.GetSharedTable(); 22 | _worksheet = _sheet.WorkbookPart.WorksheetParts.First().Worksheet; 23 | var sheetData = _worksheet.GetFirstChild(); 24 | _currentSheet = new SheetWrapper(sheetData, SharedString); 25 | 26 | IsReadyToWork = SharedString != null && _worksheet != null && _currentSheet.IsReadyToWork; 27 | } 28 | catch 29 | { 30 | _sheet.Dispose(); 31 | IsReadyToWork = false; 32 | } 33 | } 34 | } 35 | 36 | #region Fields 37 | 38 | private readonly WrapperMod _wrapperMod; 39 | 40 | private readonly string _path; 41 | 42 | SpreadsheetDocument? _sheet; 43 | private readonly Worksheet _worksheet; 44 | 45 | private readonly SheetWrapper _currentSheet; 46 | 47 | #endregion Fields 48 | #region Properties 49 | 50 | public OpenXmlElementList ChildElementsOfCurrentSheet => _currentSheet?.ChildElements; 51 | 52 | public SharedStringTablePart SharedString { get; } 53 | 54 | public bool IsReadyToWork { get; } 55 | 56 | public SpreadsheetDocument? Document => _sheet; 57 | 58 | #endregion Properties 59 | #region Methods 60 | 61 | public void NewRow() 62 | => _currentSheet.NewRow(); 63 | 64 | public void AddCell(string text, CellStyle cellStyle = CellStyle.Base) 65 | => _currentSheet.AddCell(text, cellStyle); 66 | 67 | public void AddCell(SharedStringItem textItem, CellStyle cellStyle = CellStyle.Base) 68 | => _currentSheet.AddCell(textItem, cellStyle); 69 | 70 | public void Save() 71 | { 72 | SharedString.SharedStringTable?.Save(); 73 | _sheet.WorkbookPart.Workbook.RemoveAllChildren(); 74 | _sheet.WorkbookPart.Workbook.Save(); 75 | _worksheet.Save(); 76 | _sheet.Save(); 77 | } 78 | 79 | void IDisposable.Dispose() 80 | { 81 | if (IsReadyToWork) 82 | { 83 | _sheet.Dispose(); 84 | if (_wrapperMod != WrapperMod.Import) 85 | RepairKit.RemovePrefixs(_path); 86 | } 87 | } 88 | 89 | #endregion Methods 90 | } 91 | 92 | public enum WrapperMod 93 | { 94 | Import, 95 | ExportAdd, 96 | ExportNewFile, 97 | ExportTemplateFile 98 | } 99 | 100 | public enum CellStyle 101 | { 102 | Default = 0, 103 | Base = 1, 104 | WordWrap = 2, 105 | YellowSolid = 3, 106 | RedSolid = 4, 107 | GreenSolid = 5, 108 | Context = 6, 109 | GraySolid = 7 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/Wrappers/ExportHierarchyWrapper.cs: -------------------------------------------------------------------------------- 1 | using LocalizationTracker.Data; 2 | using LocalizationTracker.Excel; 3 | using LocalizationTracker.Windows; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | 7 | namespace LocalizationTracker.Logic.Excel 8 | { 9 | class ExportHierarchyWrapper : ExportWrapper 10 | { 11 | public ExportHierarchyWrapper(IExporter exporter, ExportData data) 12 | : base(exporter, data) { } 13 | 14 | int _currentFile; 15 | int _targetFiles; 16 | float _percentPerFile; 17 | 18 | protected override void ExportInternal() 19 | { 20 | var exportEntries = SplitIntoFolders(_data.Items); 21 | _currentFile = 0; 22 | _targetFiles = exportEntries.Length; 23 | _percentPerFile = 1f / _targetFiles; 24 | 25 | var baseRoot = _data.DistFolder; 26 | foreach (var entry in exportEntries) 27 | { 28 | _currentFile++; 29 | var path = GetExportPath(baseRoot, entry.EntryName, _data.ExportParams.Source.Code, _data.ExportParams.Target[0].Code); 30 | _data.DistFolder = path; 31 | _data.Items = entry.Strings; 32 | var result = _exporter.Export(_data, SendProgress); 33 | ExportResult.Append(result); 34 | } 35 | } 36 | 37 | private void SendProgress(int current, int target) 38 | { 39 | var doneFilesProgress = (_currentFile - 1) * _percentPerFile; 40 | var currentProgress = doneFilesProgress + _percentPerFile * current / target; 41 | var progressText = $"Export file: {_currentFile}/{_targetFiles}\nExport line: {current}/{target}"; 42 | _progressReporter.Report(new ProgressState(progressText, currentProgress)); 43 | } 44 | 45 | string GetExportPath(string rootPath, string entryFolder, string fromLang, string toLang, bool removeOldFile = true) 46 | { 47 | var entryName = Path.GetFileName(entryFolder); 48 | var folderPath = Path.Combine(rootPath, Path.GetDirectoryName(entryFolder)); 49 | if (!Directory.Exists(folderPath)) 50 | Directory.CreateDirectory(folderPath); 51 | 52 | var fileName = $"{entryName}_{fromLang}_{toLang}.xlsx"; 53 | var filePath = Path.Combine(folderPath, fileName); 54 | if (removeOldFile && File.Exists(filePath)) 55 | File.Delete(filePath); 56 | 57 | return filePath; 58 | } 59 | 60 | ExportEntry[] SplitIntoFolders(StringEntry[] selectedStrings) 61 | { 62 | var sortedEntries = GetSortedStrings(selectedStrings); 63 | List exportEntries = new List(); 64 | foreach (var pair in sortedEntries) 65 | { 66 | if (pair.Value.Count > 0) 67 | { 68 | var entry = new ExportEntry(pair.Key, pair.Value.ToArray()); 69 | exportEntries.Add(entry); 70 | } 71 | } 72 | 73 | return exportEntries.ToArray(); 74 | } 75 | 76 | static Dictionary> GetSortedStrings(StringEntry[] selectedStrings) 77 | { 78 | Dictionary> _folderSortedEntries = new Dictionary>(); 79 | foreach (var entry in selectedStrings) 80 | { 81 | var strPath = entry.PathRelativeToStringsFolder; 82 | var folder = Path.GetDirectoryName(strPath); 83 | if (!_folderSortedEntries.TryGetValue(folder, out var list)) 84 | { 85 | list = new List(); 86 | _folderSortedEntries.Add(folder, list); 87 | } 88 | 89 | list.Add(entry); 90 | } 91 | 92 | 93 | return _folderSortedEntries; 94 | } 95 | } 96 | 97 | public class ExportEntry 98 | { 99 | public ExportEntry(string name, StringEntry[] strings) 100 | { 101 | EntryName = name; 102 | Strings = strings; 103 | } 104 | 105 | public string EntryName { get; } 106 | public StringEntry[] Strings { get; } 107 | } 108 | 109 | } -------------------------------------------------------------------------------- /LocalizationTracker/Logic/Excel/Wrappers/ExportSingleFileWrapper.cs: -------------------------------------------------------------------------------- 1 | using LocalizationTracker.Windows; 2 | 3 | namespace LocalizationTracker.Logic.Excel.Wrappers 4 | { 5 | class ExportSingleFileWrapper : ExportWrapper 6 | { 7 | public ExportSingleFileWrapper(IExporter exporter, ExportData data) 8 | : base(exporter, data) { } 9 | 10 | protected override void ExportInternal() 11 | { 12 | var result = _exporter.Export(_data, SendProgress); 13 | ExportResult.Append(result); 14 | } 15 | 16 | private void SendProgress(int current, int target) 17 | { 18 | var progress = $"Export line: {current}/{target}"; 19 | _progressReporter.Report(new ProgressState(progress, (float)current / target)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/ImportDiffExporter.cs: -------------------------------------------------------------------------------- 1 | using LocalizationTracker.Data; 2 | using LocalizationTracker.Excel; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using wpf4gp; 7 | 8 | namespace LocalizationTracker.Logic 9 | { 10 | static class ImportDiffExporter 11 | { 12 | static ExcelStyles ExcelStyles = new ExcelStyles(); 13 | 14 | static ColumnSettings[] _columnSettings = new[] 15 | { 16 | new ColumnSettings() 17 | { 18 | MinIndex = 5, 19 | MaxIndex = 7, 20 | Width = 60 21 | } 22 | }; 23 | 24 | public static void SaveResultAsFile(ImportResult result) 25 | { 26 | if (TryGetPath(true, out var rootPath)) 27 | { 28 | SaveResultAsFile(result, rootPath); 29 | } 30 | } 31 | 32 | public static void SaveResultsAsFile(ICollection results) 33 | { 34 | if (results.Count == 0) 35 | return; 36 | 37 | if (TryGetPath(results.Count == 1, out var rootPath)) 38 | { 39 | foreach (var result in results) 40 | { 41 | SaveResultAsFile(result, rootPath); 42 | } 43 | } 44 | } 45 | 46 | static void SaveResultAsFile(ImportResult result, string rootPath) 47 | { 48 | try 49 | { 50 | var path = Path.Combine(rootPath, $"{result.LanguageGroup.Replace(':', '_')}_diffs.xlsx"); 51 | ExportResults(path, result); 52 | } 53 | catch (Exception exc) 54 | { 55 | exc.ShowMessageBox(); 56 | } 57 | } 58 | 59 | static void ExportResults(string path, ImportResult result) 60 | { 61 | using (var wrapper = new WorkbookWrapper(path, WrapperMod.ExportNewFile, _columnSettings)) 62 | { 63 | SheetDataExtentions.NextRowID = 0; 64 | 65 | AddHeader(wrapper); 66 | var entries = result.ImportEntries; 67 | for (uint i = 0; i < entries.Count; i++) 68 | { 69 | var pair = entries[(int)i]; 70 | AddImportEntry(pair, wrapper); 71 | } 72 | 73 | wrapper.Save(); 74 | } 75 | } 76 | 77 | static void AddHeader(WorkbookWrapper wrapper) 78 | { 79 | wrapper.NewRow(); 80 | 81 | wrapper.AddCell("Key"); 82 | wrapper.AddCell("Path"); 83 | wrapper.AddCell("Status"); 84 | wrapper.AddCell("Messages"); 85 | wrapper.AddCell("Source"); 86 | wrapper.AddCell("Old Target"); 87 | wrapper.AddCell("Result"); 88 | } 89 | 90 | static void AddImportEntry(ImportEntry entry, WorkbookWrapper wrapper) 91 | { 92 | wrapper.NewRow(); 93 | 94 | wrapper.AddCell(entry.Key); 95 | wrapper.AddCell(entry.Path); 96 | wrapper.AddCell(entry.Status.ToString(), GetStyleForStatus(entry.Status)); 97 | wrapper.AddCell(entry.Messages); 98 | wrapper.AddCell(ExcelStyles.DiffToSharedString(entry.SourceDiffs), CellStyle.WordWrap); 99 | wrapper.AddCell(ExcelStyles.DiffToSharedString(entry.TargetDiffs), CellStyle.WordWrap); 100 | wrapper.AddCell(ExcelStyles.DiffToSharedString(entry.ResultDiffs), CellStyle.WordWrap); 101 | } 102 | 103 | static CellStyle GetStyleForStatus(ImportStatus status) 104 | { 105 | switch (status) 106 | { 107 | case ImportStatus.Ok: 108 | return CellStyle.GreenSolid; 109 | 110 | case ImportStatus.Warning: 111 | return CellStyle.YellowSolid; 112 | 113 | case ImportStatus.Error: 114 | return CellStyle.RedSolid; 115 | 116 | default: 117 | return CellStyle.Base; 118 | } 119 | } 120 | 121 | static bool TryGetPath(bool isSingle, out string path) 122 | { 123 | using (var dlg = new System.Windows.Forms.FolderBrowserDialog()) 124 | { 125 | dlg.SelectedPath = Directory.GetCurrentDirectory(); 126 | dlg.Description = $"Укажите папку для {(!isSingle ? "файлов" : "файла")} сравнения"; 127 | if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) 128 | { 129 | path = dlg.SelectedPath; 130 | return true; 131 | } 132 | } 133 | 134 | path = string.Empty; 135 | return false; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/JsonImporter.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml.Spreadsheet; 2 | using LocalizationTracker.Data; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.Json; 7 | 8 | namespace LocalizationTracker.Logic 9 | { 10 | public class JsonImporter : IImporter 11 | { 12 | private Locale m_TargetLocale; 13 | 14 | private string m_Key; 15 | 16 | private string m_Result; 17 | 18 | public ImportResult Import(string fileName) 19 | { 20 | var results = new List(); 21 | JsonLocalizationData strings; 22 | 23 | m_TargetLocale = Locale.Values.Where(w => w.Code == Path.GetFileNameWithoutExtension(fileName)).FirstOrDefault(); 24 | 25 | using (var sr = new StreamReader(fileName)) 26 | { 27 | var text = sr.ReadToEnd(); 28 | strings = JsonSerializer.Deserialize(text); 29 | } 30 | 31 | foreach (var str in strings.Strings) 32 | { 33 | var result = ImportString(str); 34 | 35 | if (result != null && !string.IsNullOrEmpty(result.Key)) 36 | { 37 | result.MakeDiffs(); 38 | results.Add(result); 39 | } 40 | } 41 | 42 | var importResult = new ImportResult(fileName, $"\"\":{m_TargetLocale.Code}", results); 43 | return importResult; 44 | } 45 | 46 | private ImportEntry ImportString(KeyValuePair str) 47 | { 48 | var result = new ImportEntry(); 49 | StringEntry se = null; 50 | 51 | if (!string.IsNullOrEmpty(str.Key)) 52 | { 53 | result.Key = str.Key; 54 | if (StringManager.StringsByKey.TryGetValue(result.Key, out se)) 55 | { 56 | se.Reload(); 57 | result.Path = se.AbsolutePath; 58 | result.ImportResult = se.Data.GetText(m_TargetLocale); 59 | } 60 | } 61 | 62 | result.ImportSource = ""; 63 | result.ImportTarget = ""; 64 | 65 | if (!string.IsNullOrEmpty(str.Value)) 66 | { 67 | result.ImportResult = str.Value; 68 | } 69 | 70 | if (string.IsNullOrEmpty(str.Key)) 71 | { 72 | result.Status = ImportStatus.Error; 73 | result.AddMessage("String key is empty"); 74 | } 75 | else if (se == null) 76 | { 77 | result.Status = ImportStatus.Error; 78 | result.AddMessage("Key doesn't exist in database."); 79 | } 80 | 81 | if (string.IsNullOrEmpty(str.Value)) 82 | { 83 | result.Status = ImportStatus.Error; 84 | result.AddMessage("Imported string is empty"); 85 | } 86 | 87 | if (se == null || result.Status == ImportStatus.Error) 88 | { 89 | result.Status = ImportStatus.Error; 90 | return result; 91 | } 92 | 93 | se.Data.UpdateTranslation(m_TargetLocale, result.ImportResult, null, result.ImportSource); 94 | 95 | se.Save(); 96 | 97 | return result; 98 | } 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/KnownTraits.cs: -------------------------------------------------------------------------------- 1 | namespace LocalizationTracker.Logic; 2 | 3 | public class KnownTraits 4 | { 5 | public static readonly string Draft = "Draft"; 6 | public static readonly string ToTranslate = "ToTranslate"; 7 | public static readonly string SentToTr = "SentToTr"; 8 | public static readonly string Translated = "Translated"; 9 | public static readonly string SentToEd = "SentToEd"; 10 | public static readonly string Edited = "Edited"; 11 | public static readonly string MajorChange = "MajorChange"; 12 | public static readonly string EditorChange = "ChangedInEngine"; 13 | } -------------------------------------------------------------------------------- /LocalizationTracker/Logic/LocalizationDiffExporter.cs: -------------------------------------------------------------------------------- 1 | using LocalizationTracker.Data; 2 | using LocalizationTracker.Excel; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using wpf4gp; 7 | 8 | namespace LocalizationTracker.Logic 9 | { 10 | static class LocalizationDiffExporter 11 | { 12 | static ColumnSettings[] _columnSettings = new[] 13 | { 14 | new ColumnSettings() 15 | { 16 | MinIndex = 5, 17 | MaxIndex = 7, 18 | Width = 60 19 | } 20 | }; 21 | 22 | public static void SaveResultAsFile(ImportResult result) 23 | { 24 | if (TryGetPath(true, out var rootPath)) 25 | { 26 | SaveResultAsFile(result, rootPath); 27 | } 28 | } 29 | 30 | public static void SaveResultsAsFile(ICollection results) 31 | { 32 | if (results.Count == 0) 33 | return; 34 | 35 | if (TryGetPath(results.Count == 1, out var rootPath)) 36 | { 37 | foreach (var result in results) 38 | { 39 | SaveResultAsFile(result, rootPath); 40 | } 41 | } 42 | } 43 | 44 | static void SaveResultAsFile(ImportResult result, string rootPath) 45 | { 46 | try 47 | { 48 | var path = Path.Combine(rootPath, $"{result.LanguageGroup.Replace(':', '_')}_diffs.xlsx"); 49 | ExportResults(path, result); 50 | } 51 | catch (Exception exc) 52 | { 53 | exc.ShowMessageBox(); 54 | } 55 | } 56 | 57 | static void ExportResults(string path, ImportResult result) 58 | { 59 | using (var wrapper = new WorkbookWrapper(path, WrapperMod.ExportNewFile, _columnSettings)) 60 | { 61 | SheetDataExtentions.NextRowID = 0; 62 | 63 | AddHeader(wrapper); 64 | var entries = result.ImportEntries; 65 | for (uint i = 0; i < entries.Count; i++) 66 | { 67 | var pair = entries[(int)i]; 68 | AddImportEntry(pair, wrapper); 69 | } 70 | 71 | wrapper.Save(); 72 | } 73 | } 74 | 75 | static void AddHeader(WorkbookWrapper wrapper) 76 | { 77 | wrapper.NewRow(); 78 | 79 | wrapper.AddCell("Key"); 80 | wrapper.AddCell("Path"); 81 | wrapper.AddCell("Status"); 82 | wrapper.AddCell("Messages"); 83 | wrapper.AddCell("Source"); 84 | wrapper.AddCell("Old Target"); 85 | wrapper.AddCell("Result"); 86 | } 87 | 88 | static void AddImportEntry(ImportEntry entry, WorkbookWrapper wrapper) 89 | { 90 | wrapper.NewRow(); 91 | 92 | wrapper.AddCell(entry.Key); 93 | wrapper.AddCell(entry.Path); 94 | wrapper.AddCell(entry.Status.ToString(), GetStyleForStatus(entry.Status)); 95 | wrapper.AddCell(entry.Messages); 96 | wrapper.AddCell(ExcelStyles.DiffToSharedString(entry.SourceDiffs), CellStyle.WordWrap); 97 | wrapper.AddCell(ExcelStyles.DiffToSharedString(entry.TargetDiffs), CellStyle.WordWrap); 98 | wrapper.AddCell(ExcelStyles.DiffToSharedString(entry.ResultDiffs), CellStyle.WordWrap); 99 | } 100 | 101 | static CellStyle GetStyleForStatus(ImportStatus status) 102 | { 103 | switch (status) 104 | { 105 | case ImportStatus.Ok: 106 | return CellStyle.GreenSolid; 107 | 108 | case ImportStatus.Warning: 109 | return CellStyle.YellowSolid; 110 | 111 | case ImportStatus.Error: 112 | return CellStyle.RedSolid; 113 | 114 | default: 115 | return CellStyle.Base; 116 | } 117 | } 118 | 119 | static bool TryGetPath(bool isSingle, out string path) 120 | { 121 | using (var dlg = new System.Windows.Forms.FolderBrowserDialog()) 122 | { 123 | dlg.SelectedPath = Directory.GetCurrentDirectory(); 124 | dlg.Description = $"Укажите папку для {(!isSingle ? "файлов" : "файла")} сравнения"; 125 | if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) 126 | { 127 | path = dlg.SelectedPath; 128 | return true; 129 | } 130 | } 131 | 132 | path = string.Empty; 133 | return false; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /LocalizationTracker/Logic/OpenOffice/OpenOfficeXmlHelper.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Xml; 5 | 6 | namespace LocalizationTracker.OpenOffice 7 | { 8 | public static class OpenOfficeXmlHelper 9 | { 10 | public static XmlNode GetTable(this XmlDocument document) 11 | => document 12 | ["office:document-content"]? 13 | ["office:body"]? 14 | ["office:spreadsheet"]? 15 | ["table:table"]; 16 | 17 | public static IEnumerable GetRows(this XmlNode table) 18 | => table.OfType().Where(n => n.Name == "table:table-row"); 19 | 20 | public static IEnumerable GetCells(this XmlNode row) 21 | => row.OfType().Where(n => n.Name == "table:table-cell"); 22 | 23 | public static XmlNode GetText(this XmlNode cell) 24 | => cell.OfType().FirstOrDefault(i => i.Name == "text:p"); 25 | 26 | [CanBeNull] 27 | public static string ReadCellValueAt(this IEnumerable source, int i) 28 | { 29 | int ci = 0; 30 | foreach (var cell in source) 31 | { 32 | var repeatAttribute = cell.Attributes?.GetNamedItem("table:number-columns-repeated"); 33 | if (repeatAttribute != null && 34 | int.TryParse(repeatAttribute.InnerText, out int repeat) && 35 | repeat > 0) 36 | { 37 | ci += repeat; 38 | } 39 | else 40 | { 41 | ci += 1; 42 | } 43 | 44 | if (ci > i) 45 | { 46 | return cell.GetText()?.InnerText ?? ""; 47 | } 48 | } 49 | 50 | return null; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /LocalizationTracker/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | FileSystem 8 | Release 9 | publish\win-x64\ 10 | Any CPU 11 | net7.0-windows 12 | win-x64 13 | true 14 | false 15 | 16 | -------------------------------------------------------------------------------- /LocalizationTracker/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace LocalizationTracker.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LocalizationTracker.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /LocalizationTracker/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | template.xlsx 7 | 8 | 9 | ../Project Kingmaker/Strings 10 | 11 | 12 | ../Project Kingmaker/Assets 13 | 14 | 15 | 100 16 | 17 | 18 | 100 19 | 20 | 21 | 1500 22 | 23 | 24 | 800 25 | 26 | 27 | 50 28 | 29 | 30 | 50 31 | 32 | 33 | 800 34 | 35 | 36 | 1000 37 | 38 | 39 | Normal 40 | 41 | 42 | Normal 43 | 44 | 45 | Normal 46 | 47 | 48 | 200 49 | 50 | 51 | 200 52 | 53 | 54 | 600 55 | 56 | 57 | Normal 58 | 59 | 60 | 1500 61 | 62 | 63 | 500 64 | 65 | 66 | 500 67 | 68 | 69 | 300 70 | 71 | 72 | 150 73 | 74 | 75 | 76 | 77 | 78 | template.ods 79 | 80 | 81 | -------------------------------------------------------------------------------- /LocalizationTracker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "LocalizationTracker": { 4 | "commandName": "Project", 5 | "commandLineArgs": "config.json", 6 | "workingDirectory": "d:\\pf2-production-2\\Localization\\" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /LocalizationTracker/Tools/Diff.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Windows; 5 | using System.Windows.Documents; 6 | using System.Windows.Media; 7 | using DiffMatchPatch; 8 | using JetBrains.Annotations; 9 | using LocalizationTracker.Components; 10 | 11 | namespace LocalizationTracker.Tools 12 | { 13 | public static class Diff 14 | { 15 | [NotNull] 16 | public static InlinesWrapper MakeInlines(string before, string after) 17 | { 18 | if (before == null) 19 | before = ""; 20 | if (after == null) 21 | after = ""; 22 | 23 | List diffs; 24 | try 25 | { 26 | diffs = DiffMatchPatchModule.Default.DiffMain(before, after); 27 | DiffMatchPatchModule.Default.DiffCleanupSemantic(diffs); 28 | DiffMatchPatchModule.Default.DiffCleanupEfficiency(diffs); 29 | } 30 | catch (Exception x) 31 | { 32 | throw new Exception($"Error when diffing strings:\n before <{before}>\nafter <{after}>", x); 33 | } 34 | 35 | var inlines = diffs 36 | .Select(MakeInline) 37 | .ToArray(); 38 | 39 | return new InlinesWrapper(inlines); 40 | } 41 | 42 | [NotNull] 43 | private static InlineTemplate MakeInline(DiffMatchPatch.Diff diff) 44 | { 45 | var run = new InlineTemplate(diff.Text); 46 | if (Equals(diff.Operation, Operation.Delete)) 47 | { 48 | run.Background = ColorUtility.Red; 49 | run.StrikeThrough = true; 50 | run.InlineType = InlineType.DiffDelete; 51 | } 52 | else if (Equals(diff.Operation, Operation.Insert)) 53 | { 54 | run.Background = ColorUtility.Green; 55 | run.InlineType = InlineType.DiffInsert; 56 | } 57 | return run; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /LocalizationTracker/Tools/FilterFits.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows; 4 | using System.Windows.Media; 5 | using JetBrains.Annotations; 6 | using LocalizationTracker.Components; 7 | using LocalizationTracker.Logic; 8 | using LocalizationTracker.Windows; 9 | 10 | namespace LocalizationTracker.Tools; 11 | 12 | public class FilterFits 13 | { 14 | [NotNull] 15 | public static InlinesWrapper MakeInlines(string text, Color? selectedColor = null) 16 | { 17 | return MakeInlines(new[] { new InlineTemplate(text) }, selectedColor); 18 | } 19 | 20 | public static InlinesWrapper MakeInlines(InlineTemplate[] inlineTemplates, Color? selectedColor = null) 21 | { 22 | if (string.IsNullOrWhiteSpace(StringsFilter.Filter.Text)) return new InlinesWrapper(inlineTemplates); 23 | 24 | var result = new List(); 25 | 26 | var searchText = StringsFilter.Filter.Text; 27 | 28 | if (StringsFilter.Filter.IgnoreCase == true) 29 | { 30 | searchText = searchText.ToLower(); 31 | } 32 | 33 | int index; 34 | 35 | Color? filterColor = selectedColor ?? Brushes.LightBlue.Color; 36 | 37 | foreach (var i in inlineTemplates) 38 | { 39 | if (string.IsNullOrWhiteSpace(StringsFilter.Filter.Text)) searchText = ""; 40 | 41 | var text = StringsFilter.Filter.IgnoreCase == true ? i.Text.ToLower() : i.Text; 42 | var foregroundColor = i.Foreground; 43 | Color? backgroundColor = i.Background; 44 | bool strikeThrough = i.StrikeThrough; 45 | FontWeight? fontWeight = i.FontWeight; 46 | 47 | if (text.Contains(searchText)) 48 | { 49 | int startIndex = 0; 50 | 51 | while ((index = text.IndexOf(searchText, startIndex)) != -1) 52 | { 53 | if (index > startIndex) 54 | { 55 | string beforeMatch = i.Text.Substring(startIndex, index - startIndex); 56 | result.Add(MakeInline(beforeMatch, foregroundColor, backgroundColor, strikeThrough, fontWeight)); 57 | } 58 | 59 | string match = i.Text.Substring(index, searchText.Length); 60 | 61 | if (!backgroundColor.HasValue) 62 | { 63 | result.Add(MakeInline(match, foregroundColor, filterColor, strikeThrough, fontWeight)); 64 | } 65 | else 66 | { 67 | result.Add(MakeInline(match, foregroundColor, backgroundColor, strikeThrough, fontWeight)); 68 | } 69 | 70 | startIndex = index + searchText.Length; 71 | } 72 | 73 | if (startIndex < i.Text.Length) 74 | { 75 | string remaining = i.Text.Substring(startIndex); 76 | result.Add(MakeInline(remaining, foregroundColor, backgroundColor, strikeThrough, fontWeight)); 77 | } 78 | } 79 | else 80 | { 81 | result.Add(i); 82 | } 83 | 84 | } 85 | 86 | return new InlinesWrapper(result.ToArray()); 87 | } 88 | 89 | private static InlineTemplate MakeInline(string text, 90 | Color? foregroundColor = null, 91 | Color? color = null, 92 | bool strikeThrough = false, 93 | FontWeight? fontWeight = null) 94 | => new(text) 95 | { 96 | Background = color, 97 | Foreground = foregroundColor, 98 | StrikeThrough = strikeThrough, 99 | FontWeight = fontWeight 100 | }; 101 | 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/DataStructure/TermEntry.cs: -------------------------------------------------------------------------------- 1 | namespace LocalizationTracker.Tools.GlossaryTools; 2 | 3 | /// 4 | /// Term entry represents piece of text in LocaleEntry of StringEntry 5 | /// 6 | public class TermEntry 7 | { 8 | public string TermId; 9 | public string StringKey; 10 | public string Comment; 11 | public int StartIndex; 12 | public int EndIndex; 13 | public bool CaseError; 14 | public int Length => EndIndex - StartIndex + 1; 15 | } -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/DataStructure/TermTemplateCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LocalizationTracker.Tools.GlossaryTools; 4 | 5 | public class TermTemplateCollection 6 | { 7 | private Dictionary>> m_IdToLocaleToTemplateMap = new(); 8 | 9 | public void AddTermTemplate(string termId, Locale locale, TermTemplate template) 10 | { 11 | if (!m_IdToLocaleToTemplateMap.ContainsKey(termId)) 12 | m_IdToLocaleToTemplateMap[termId] = new Dictionary>(); 13 | 14 | if (!m_IdToLocaleToTemplateMap[termId].ContainsKey(locale)) 15 | m_IdToLocaleToTemplateMap[termId][locale] = new (){}; 16 | 17 | m_IdToLocaleToTemplateMap[termId][locale].Add(template); 18 | } 19 | 20 | public bool TryGetTermTemplates(string termId, Locale locale, out List template) 21 | { 22 | template = new List(); 23 | return m_IdToLocaleToTemplateMap.TryGetValue(termId, out var locales) && 24 | locales.TryGetValue(locale, out template); 25 | } 26 | } -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/BakingSheet.Json.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BakingSheet.Json", 3 | "rootNamespace": "", 4 | "references": [ 5 | "BakingSheet" 6 | ], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/BakingSheet.Json.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: df02fc2854da34e60839910beb076b2b 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/JsonSheetAssetPathConverter.cs: -------------------------------------------------------------------------------- 1 | // BakingSheet, Maxwell Keonwoo Kang , 2022 2 | 3 | using System; 4 | using Newtonsoft.Json; 5 | 6 | namespace Cathei.BakingSheet 7 | { 8 | public class JsonSheetAssetPathConverter : JsonConverter 9 | { 10 | public override ISheetAssetPath ReadJson(JsonReader reader, Type objectType, ISheetAssetPath existingValue, bool hasExistingValue, JsonSerializer serializer) 11 | { 12 | string path = (string)reader.Value; 13 | return (ISheetAssetPath)Activator.CreateInstance(objectType, path); 14 | } 15 | 16 | public override void WriteJson(JsonWriter writer, ISheetAssetPath value, JsonSerializer serializer) 17 | { 18 | if (!value.IsValid()) 19 | { 20 | writer.WriteNull(); 21 | return; 22 | } 23 | 24 | writer.WriteValue(value.RawValue); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/JsonSheetAssetPathConverter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 23aa15708e86242549c7a3f76a4ad3b8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/JsonSheetContractResolver.cs: -------------------------------------------------------------------------------- 1 | // BakingSheet, Maxwell Keonwoo Kang , 2022 2 | 3 | using System; 4 | using System.Reflection; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Converters; 7 | using Newtonsoft.Json.Serialization; 8 | 9 | namespace Cathei.BakingSheet 10 | { 11 | public class JsonSheetContractResolver : DefaultContractResolver 12 | { 13 | public static readonly JsonSheetContractResolver Instance = new JsonSheetContractResolver(); 14 | 15 | protected override JsonContract CreateContract(System.Type objectType) 16 | { 17 | if (typeof(ISheetRow).IsAssignableFrom(objectType) || 18 | typeof(ISheetRowElem).IsAssignableFrom(objectType)) 19 | { 20 | return CreateObjectContract(objectType); 21 | } 22 | 23 | if (typeof(ISheetReference).IsAssignableFrom(objectType)) 24 | { 25 | var contract = base.CreateContract(objectType); 26 | contract.Converter = new JsonSheetReferenceConverter(); 27 | return contract; 28 | } 29 | 30 | if (typeof(ISheetAssetPath).IsAssignableFrom(objectType)) 31 | { 32 | var contract = base.CreateContract(objectType); 33 | contract.Converter = new JsonSheetAssetPathConverter(); 34 | return contract; 35 | } 36 | 37 | if (objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true) 38 | { 39 | var contract = base.CreateContract(objectType); 40 | contract.Converter = new StringEnumConverter(); 41 | return contract; 42 | } 43 | 44 | return base.CreateContract(objectType); 45 | } 46 | 47 | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) 48 | { 49 | JsonProperty property = base.CreateProperty(member, memberSerialization); 50 | 51 | if (member is PropertyInfo pi) 52 | { 53 | var nonSerialize = pi.IsDefined(typeof(NonSerializedAttribute)); 54 | var hasSetMethod = pi.SetMethod != null; 55 | 56 | property.Writable = !nonSerialize && hasSetMethod; 57 | property.ShouldSerialize = property.ShouldDeserialize = _ => !nonSerialize && hasSetMethod; 58 | } 59 | 60 | return property; 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/JsonSheetContractResolver.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f93f015347c974920ae6ff72b64ad05d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/JsonSheetConverter.cs: -------------------------------------------------------------------------------- 1 | // BakingSheet, Maxwell Keonwoo Kang , 2022 2 | 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using Cathei.BakingSheet.Internal; 7 | using Microsoft.Extensions.Logging; 8 | using Newtonsoft.Json; 9 | using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; 10 | 11 | namespace Cathei.BakingSheet 12 | { 13 | public class JsonSheetConverter : ISheetConverter 14 | { 15 | public virtual string Extension => "json"; 16 | 17 | private string _loadPath; 18 | private IFileSystem _fileSystem; 19 | 20 | public JsonSheetConverter(string path, IFileSystem fileSystem = null) 21 | { 22 | _loadPath = path; 23 | _fileSystem = fileSystem ?? new FileSystem(); 24 | } 25 | 26 | public static void ErrorHandler(ILogger logError, ErrorEventArgs err) 27 | { 28 | if (err.ErrorContext.Member?.ToString() == nameof(ISheetRow.Id) && 29 | err.ErrorContext.OriginalObject is ISheetRow && 30 | !(err.CurrentObject is ISheet)) 31 | { 32 | // if Id has error, the error must be handled on the sheet level 33 | return; 34 | } 35 | 36 | using (logError.BeginScope(err.ErrorContext.Path)) 37 | logError.LogError(err.ErrorContext.Error, err.ErrorContext.Error.Message); 38 | 39 | err.ErrorContext.Handled = true; 40 | } 41 | 42 | public virtual JsonSerializerSettings GetSettings(ILogger logError) 43 | { 44 | var settings = new JsonSerializerSettings(); 45 | 46 | settings.Error = (_, err) => ErrorHandler(logError, err); 47 | settings.ContractResolver = JsonSheetContractResolver.Instance; 48 | 49 | return settings; 50 | } 51 | 52 | protected virtual string Serialize(object obj, Type type, ILogger logger) 53 | { 54 | return JsonConvert.SerializeObject(obj, type, GetSettings(logger)); 55 | } 56 | 57 | protected virtual object Deserialize(string json, Type type, ILogger logger) 58 | { 59 | return JsonConvert.DeserializeObject(json, type, GetSettings(logger)); 60 | } 61 | 62 | public async Task Import(SheetConvertingContext context) 63 | { 64 | var sheetProps = context.Container.GetSheetProperties(); 65 | 66 | foreach (var pair in sheetProps) 67 | { 68 | using (context.Logger.BeginScope(pair.Key)) 69 | { 70 | var path = Path.Combine(_loadPath, $"{pair.Key}.{Extension}"); 71 | 72 | if (!_fileSystem.Exists(path)) 73 | continue; 74 | 75 | string data; 76 | 77 | using (var stream = _fileSystem.OpenRead(path)) 78 | using (var reader = new StreamReader(stream)) 79 | data = await reader.ReadToEndAsync(); 80 | 81 | var sheet = Deserialize(data, pair.Value.PropertyType, context.Logger) as ISheet; 82 | pair.Value.SetValue(context.Container, sheet); 83 | } 84 | } 85 | 86 | return true; 87 | } 88 | 89 | public async Task Export(SheetConvertingContext context) 90 | { 91 | var sheetProps = context.Container.GetSheetProperties(); 92 | 93 | _fileSystem.CreateDirectory(_loadPath); 94 | 95 | foreach (var pair in sheetProps) 96 | { 97 | using (context.Logger.BeginScope(pair.Key)) 98 | { 99 | var sheet = pair.Value.GetValue(context.Container); 100 | var data = Serialize(sheet, pair.Value.PropertyType, context.Logger); 101 | 102 | var path = Path.Combine(_loadPath, $"{pair.Key}.{Extension}"); 103 | 104 | using (var stream = _fileSystem.OpenWrite(path)) 105 | using (var writer = new StreamWriter(stream)) 106 | await writer.WriteAsync(data); 107 | } 108 | } 109 | 110 | return true; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/JsonSheetConverter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 92892f2418fd347d5a0c519b1bd6b4fd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/JsonSheetReferenceConverter.cs: -------------------------------------------------------------------------------- 1 | // BakingSheet, Maxwell Keonwoo Kang , 2022 2 | 3 | using System; 4 | using Newtonsoft.Json; 5 | 6 | namespace Cathei.BakingSheet 7 | { 8 | public class JsonSheetReferenceConverter : JsonConverter 9 | { 10 | public override ISheetReference ReadJson(JsonReader reader, Type objectType, ISheetReference existingValue, bool hasExistingValue, JsonSerializer serializer) 11 | { 12 | if (existingValue == null) 13 | existingValue = (ISheetReference)Activator.CreateInstance(objectType); 14 | 15 | existingValue.Id = serializer.Deserialize(reader, existingValue.IdType); 16 | return existingValue; 17 | } 18 | 19 | public override void WriteJson(JsonWriter writer, ISheetReference value, JsonSerializer serializer) 20 | { 21 | serializer.Serialize(writer, value.Id); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/Json/JsonSheetReferenceConverter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 00e7b3552d0d434f981c7d440c619881 3 | timeCreated: 1663385507 -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/SheetImport/GlossarySheet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Cathei.BakingSheet; 3 | using DocumentFormat.OpenXml.Drawing.Diagrams; 4 | 5 | namespace LocalizationTracker.Tools.GlossaryTools; 6 | 7 | public interface IGlossary 8 | { 9 | string Id { get; } 10 | public string ruRU { get; } 11 | public string Example_ruRU { get; } 12 | public string enGB { get; } 13 | public string Example_enGB { get; } 14 | 15 | public Dictionary Example { get; } 16 | public Dictionary Exact { get; } 17 | public string Comment { get; } 18 | bool TryGetTranslation(Locale locale, out string translation); 19 | string GetExampleTranslation(Locale? locale); 20 | 21 | } 22 | 23 | 24 | public class GlossarySheet : Sheet 25 | { 26 | /// 27 | /// Table markdown 28 | /// 29 | public class Term : SheetRow, IGlossary 30 | { 31 | public string ruRU { get; private set; } 32 | public string Example_ruRU { get; private set; } 33 | public string enGB { get; private set; } 34 | public string Example_enGB { get; private set; } 35 | 36 | public Dictionary Example { get; private set; } = new Dictionary(); 37 | public Dictionary Exact { get; private set; } = new Dictionary(); 38 | public string Comment { get; private set; } 39 | 40 | public bool TryGetTranslation(Locale locale, out string translation) 41 | { 42 | translation = string.Empty; 43 | if (!Exact.TryGetValue(locale, out translation)) 44 | { 45 | if (locale == Locale.enGB) 46 | translation = enGB; 47 | if (locale == Locale.ruRU) 48 | translation = ruRU; 49 | } 50 | 51 | return !string.IsNullOrEmpty(translation); 52 | } 53 | 54 | public string GetExampleTranslation(Locale? locale) 55 | { 56 | var translation = string.Empty; 57 | if (locale == null) 58 | return Example_enGB; 59 | 60 | if (Exact.TryGetValue(locale, out translation)) 61 | return translation; 62 | 63 | if (locale == Locale.enGB) 64 | return Example_enGB; 65 | if (locale == Locale.ruRU) 66 | return Example_ruRU; 67 | 68 | return Example_enGB; 69 | } 70 | } 71 | 72 | public class AmberGlossarySheet : Sheet 73 | { 74 | /// 75 | /// Table markdown 76 | /// 77 | public class AmberTerm : SheetRow, IGlossary 78 | { 79 | public string ruRU { get; private set; } 80 | public string Example_ruRU { get; private set; } 81 | public string enUS { get; private set; } 82 | public string Example_enUS { get; private set; } 83 | public string Description { get; private set; } 84 | public string enGB => enUS; 85 | public string Example_enGB => Example_enUS; 86 | public string Comment => Description; 87 | 88 | public Dictionary Example { get; private set; } = new Dictionary(); 89 | public Dictionary Exact { get; private set; } = new Dictionary(); 90 | 91 | public bool TryGetTranslation(Locale locale, out string translation) 92 | { 93 | translation = string.Empty; 94 | if (!Exact.TryGetValue(locale, out translation)) 95 | { 96 | if (locale == Locale.enUS) 97 | translation = enUS; 98 | if (locale == Locale.ruRU) 99 | translation = ruRU; 100 | } 101 | 102 | return !string.IsNullOrEmpty(translation); 103 | } 104 | 105 | public string GetExampleTranslation(Locale? locale) 106 | { 107 | var translation = string.Empty; 108 | if (locale == null) 109 | return Example_enUS; 110 | 111 | if (Exact.TryGetValue(locale, out translation)) 112 | return translation; 113 | 114 | if (locale == Locale.enUS) 115 | return Example_enUS; 116 | if (locale == Locale.ruRU) 117 | return Example_ruRU; 118 | 119 | return Example_enUS; 120 | } 121 | } 122 | } 123 | } 124 | 125 | 126 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/SheetImport/GlossarySheetContainer.cs: -------------------------------------------------------------------------------- 1 | using Cathei.BakingSheet; 2 | using static LocalizationTracker.Tools.GlossaryTools.GlossarySheet; 3 | 4 | namespace LocalizationTracker.Tools.GlossaryTools; 5 | 6 | public class GlossarySheetContainer : SheetContainerBase 7 | { 8 | public GlossarySheetContainer(Microsoft.Extensions.Logging.ILogger logger) : base(logger) { } 9 | public GlossarySheet Main { get; set; } 10 | 11 | } 12 | 13 | public class AmberGlossarySheetContainer : SheetContainerBase 14 | { 15 | public AmberGlossarySheetContainer(Microsoft.Extensions.Logging.ILogger logger) : base(logger) { } 16 | public AmberGlossarySheet Main { get; set; } 17 | } -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/SheetImport/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace LocalizationTracker.Tools.GoogleSheet; 6 | 7 | public class Logger : ILogger 8 | { 9 | public static readonly Logger Default = new Logger(); 10 | 11 | private IExternalScopeProvider scopeProvider = new LoggerExternalScopeProvider(); 12 | 13 | private IList scopes = new List(); 14 | 15 | public IDisposable BeginScope(TState state) 16 | { 17 | return scopeProvider.Push(state); 18 | } 19 | 20 | public bool IsEnabled(LogLevel logLevel) 21 | { 22 | return true; 23 | } 24 | 25 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 26 | { 27 | scopes.Clear(); 28 | scopeProvider.ForEachScope((x, scopes) => scopes.Add(x), scopes); 29 | 30 | var message = formatter(state, exception); 31 | if (scopes.Count > 0) 32 | message = $"[{string.Join(">", scopes)}] {message}"; 33 | 34 | switch (logLevel) 35 | { 36 | case LogLevel.Trace: 37 | case LogLevel.Debug: 38 | case LogLevel.Information: 39 | Console.WriteLine(message); 40 | break; 41 | 42 | case LogLevel.Warning: 43 | Console.WriteLine(message); 44 | break; 45 | 46 | case LogLevel.Error: 47 | case LogLevel.Critical: 48 | Console.WriteLine(message); 49 | break; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /LocalizationTracker/Tools/GlossaryTools/SheetImport/SheetImporter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using System.Windows; 4 | using Cathei.BakingSheet; 5 | using DocumentFormat.OpenXml.Spreadsheet; 6 | using LocalizationTracker.Tools.GoogleSheet; 7 | 8 | namespace LocalizationTracker.Tools.GlossaryTools; 9 | 10 | public class SheetImporter 11 | { 12 | public async void ReadSheetFromGoogle() 13 | { 14 | await ReadSheetAsync(); 15 | } 16 | 17 | public async void ReadSheetFromJson() 18 | { 19 | await ReadSheetFromDisc(); 20 | } 21 | 22 | private async Task ReadSheetAsync() 23 | { 24 | var logger = new Logger(); 25 | SheetContainerBase sheetContainer = AppConfig.Instance.Project == "Amber" ? new AmberGlossarySheetContainer(logger) : new GlossarySheetContainer(logger); 26 | 27 | if (!File.Exists(AppConfig.Instance.Glossary.GoogleCredentialPath)) 28 | { 29 | MessageBox.Show("Glossary can't find Credentials!", "Glossary error", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.Yes); 30 | return; 31 | } 32 | 33 | string googleCredential = await File.ReadAllTextAsync(AppConfig.Instance.Glossary.GoogleCredentialPath); 34 | var googleConverter = new GoogleSheetConverter(AppConfig.Instance.Glossary.GlossaryGSheetId, googleCredential); 35 | 36 | await sheetContainer.Bake(googleConverter); 37 | 38 | var jsonConverter = new JsonSheetConverter(AppConfig.Instance.Glossary.GlossaryJSONPath); 39 | 40 | if (sheetContainer is GlossarySheetContainer glossaryContainer) 41 | { 42 | if (glossaryContainer.Main != null) 43 | { 44 | await sheetContainer.Store(jsonConverter); 45 | Glossary.Instance.Initialize(glossaryContainer.Main, true); 46 | MessageBox.Show($"Glossary updated! \n Total terms: {glossaryContainer.Main.Count}", 47 | "Glossary updated!", MessageBoxButton.OK, MessageBoxImage.Information, MessageBoxResult.Yes); 48 | } 49 | else 50 | { 51 | MessageBox.Show("Glossary Main sheet is empty!", "Glossary error", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.Yes); 52 | } 53 | } 54 | else if (sheetContainer is AmberGlossarySheetContainer amberContainer) 55 | { 56 | if (amberContainer.Main != null) 57 | { 58 | await sheetContainer.Store(jsonConverter); 59 | Glossary.Instance.Initialize(amberContainer.Main, true); 60 | MessageBox.Show($"Glossary updated! \n Total terms: {amberContainer.Main.Count}", 61 | "Glossary updated!", MessageBoxButton.OK, MessageBoxImage.Information, MessageBoxResult.Yes); 62 | } 63 | else 64 | { 65 | MessageBox.Show("Glossary Main sheet is empty!", "Glossary error", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.Yes); 66 | } 67 | } 68 | } 69 | 70 | 71 | private async Task ReadSheetFromDisc() 72 | { 73 | var logger = new Logger(); 74 | SheetContainerBase sheetContainer = AppConfig.Instance.Project == "Amber" ? new AmberGlossarySheetContainer(logger) : new GlossarySheetContainer(logger); 75 | var jsonConverter = new JsonSheetConverter(AppConfig.Instance.Glossary.GlossaryJSONPath); 76 | await sheetContainer.Bake(jsonConverter); 77 | 78 | if (sheetContainer is GlossarySheetContainer glossaryContainer) 79 | { 80 | if (glossaryContainer.Main != null) 81 | { 82 | Glossary.Instance.Initialize(glossaryContainer.Main, false); 83 | 84 | } 85 | } 86 | else if (sheetContainer is AmberGlossarySheetContainer amberContainer) 87 | { 88 | if (amberContainer.Main != null) 89 | { 90 | Glossary.Instance.Initialize(amberContainer.Main, false); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /LocalizationTracker/Tools/MaxLength.cs: -------------------------------------------------------------------------------- 1 | using DiffMatchPatch; 2 | using JetBrains.Annotations; 3 | using LocalizationTracker.Components; 4 | using LocalizationTracker.Logic; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Windows; 11 | using System.Windows.Media; 12 | 13 | namespace LocalizationTracker.Tools 14 | { 15 | class MaxLength 16 | { 17 | public static InlinesWrapper MakeInlines(string text, int symbolsCount) 18 | { 19 | var result = new List(); 20 | 21 | //Color? filterColor = selectedColor ?? Brushes.LightBlue.Color; 22 | 23 | if (text.Length > symbolsCount) 24 | { 25 | var startString = text[..symbolsCount]; 26 | var sub = text[symbolsCount..]; 27 | 28 | result.Add(new InlineTemplate(startString)); 29 | result.Add(MakeInline(sub)); 30 | } 31 | else 32 | { 33 | result.Add(new InlineTemplate(text)); 34 | } 35 | 36 | return new InlinesWrapper(result.ToArray()); 37 | } 38 | 39 | private static InlineTemplate MakeInline(string text) 40 | => new(text) 41 | { 42 | Background = Brushes.Yellow.Color, 43 | }; 44 | 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LocalizationTracker/Utility/ColorUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Media; 2 | 3 | namespace LocalizationTracker 4 | { 5 | public static class ColorUtility 6 | { 7 | public static Color Red => new Color() 8 | { 9 | R = 255, 10 | G = 153, 11 | B = 153, 12 | A = 255 13 | }; 14 | 15 | public static Color Green => new Color 16 | { 17 | R = 215, 18 | G = 227, 19 | B = 188, 20 | A = 255 21 | }; 22 | 23 | public static Color ExportDiffGreen => new Color 24 | { 25 | R = 0, 26 | G = 128, 27 | B = 0, 28 | A = 255 29 | }; 30 | 31 | public static Color ExportDiffRed => new Color() 32 | { 33 | R = 255, 34 | G = 0, 35 | B = 0, 36 | A = 255 37 | }; 38 | 39 | public static Color ExportDiffYellow => new Color 40 | { 41 | R = 255, 42 | G = 255, 43 | B = 0, 44 | A = 255 45 | }; 46 | 47 | public static Color ContextYellow => new Color 48 | { 49 | R = 255, 50 | G = 255, 51 | B = 204, 52 | A = 255 53 | }; 54 | 55 | public static Color Gray => new Color 56 | { 57 | R = 220, 58 | G = 220, 59 | B = 220, 60 | A = 255 61 | }; 62 | 63 | public static DocumentFormat.OpenXml.Spreadsheet.Color MediaColorToOXMLColor(Color color) 64 | => new DocumentFormat.OpenXml.Spreadsheet.Color() { Rgb = MediaColorToHEX(color) }; 65 | 66 | public static string MediaColorToHEX(Color color) 67 | => string.Format("{1:X2}{2:X2}{3:X2}", color.A, color.R, color.G, color.B); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /LocalizationTracker/Utility/GenderSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | using System.Windows; 6 | 7 | namespace LocalizationTracker.Utility 8 | { 9 | static class GenderSystem 10 | { 11 | static Dictionary _characterGenders = new Dictionary(); 12 | 13 | static HashSet _excludedName = new HashSet() { "Names" }; 14 | 15 | static bool _wasInited = false; 16 | 17 | public static string GetNameWithGender(string name) 18 | { 19 | if (!_characterGenders.TryGetValue(name, out var gender)) 20 | gender = Gender.Unknown; 21 | return $"{name}:{gender}"; 22 | } 23 | 24 | public static void Init() 25 | { 26 | if (_wasInited) 27 | return; 28 | 29 | _wasInited = true; 30 | Rescan(); 31 | } 32 | 33 | public static void Rescan() 34 | { 35 | _characterGenders = new Dictionary(); 36 | var rootPath = AppConfig.Instance.UnitsFolder; 37 | if (!string.IsNullOrEmpty(rootPath)) 38 | { 39 | ScanFolder(rootPath); 40 | } 41 | else MessageBox.Show("В настроечном файле не указан путь до папки с ассетами юнитов. \nОпределить гендер говорящего будет невозможно"); 42 | } 43 | 44 | static void ScanFolder(string folderPath) 45 | { 46 | if (!CheckFolderIsIncluded(folderPath)) 47 | return; 48 | 49 | foreach (var file in Directory.EnumerateFiles(folderPath, "*.asset")) 50 | { 51 | var fileText = File.ReadAllText(file); 52 | Regex regex = new Regex($@"Gender: ([0-9])"); 53 | var match = regex.Match(fileText); 54 | if (match != null) 55 | { 56 | var name = Path.GetFileNameWithoutExtension(file); 57 | var gender = GetValueFromString(match.Value); 58 | if (!_characterGenders.ContainsKey(name)) 59 | _characterGenders.Add(name, (Gender)gender); 60 | } 61 | } 62 | 63 | var childs = Directory.GetDirectories(folderPath); 64 | foreach (var child in childs) 65 | ScanFolder(child); 66 | } 67 | 68 | static bool CheckFolderIsIncluded(string folderPath) 69 | { 70 | foreach (var name in _excludedName) 71 | { 72 | if (folderPath.Contains(name)) 73 | return false; 74 | } 75 | 76 | return true; 77 | } 78 | 79 | static int GetValueFromString(string input) 80 | { 81 | var builder = new StringBuilder(); 82 | int val = 0; 83 | 84 | for (int i = 0; i < input.Length; i++) 85 | { 86 | var chr = input[i]; 87 | if (char.IsDigit(chr)) 88 | builder.Append(chr); 89 | } 90 | 91 | if (builder.Length > 0) 92 | { 93 | var value = builder.ToString(); 94 | int.TryParse(value, out val); 95 | } 96 | 97 | return val; 98 | } 99 | 100 | enum Gender 101 | { 102 | Male = 0, 103 | Female = 1, 104 | Unknown = 255 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /LocalizationTracker/Utility/LinqExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace LocalizationTracker.Utility 5 | { 6 | public static class LinqExtension 7 | { 8 | public static int IndexAt(this IList source, Predicate predicate) 9 | { 10 | for (int i = 0; i < source.Count; i++) 11 | { 12 | if (predicate(source[i])) 13 | return i; 14 | } 15 | 16 | return -1; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /LocalizationTracker/Utility/ObservableRangeCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Collections.Specialized; 5 | using System.ComponentModel; 6 | using System.Linq; 7 | 8 | namespace LocalizationTracker.Utility 9 | { 10 | public class ObservableRangeCollection : ObservableCollection 11 | { 12 | private const string CountString = "Count"; 13 | private const string IndexerName = "Item[]"; 14 | 15 | protected enum ProcessRangeAction 16 | { 17 | Add, 18 | Replace, 19 | Remove 20 | }; 21 | 22 | public ObservableRangeCollection() : base() 23 | { 24 | } 25 | 26 | public ObservableRangeCollection(IEnumerable collection) : base(collection) 27 | { 28 | } 29 | 30 | public ObservableRangeCollection(List list) : base(list) 31 | { 32 | } 33 | 34 | protected virtual void ProcessRange(Span collection, ProcessRangeAction action) 35 | { 36 | if (collection == null) 37 | throw new ArgumentNullException(nameof(collection)); 38 | 39 | if (collection.Length == 0) 40 | return; 41 | 42 | CheckReentrancy(); 43 | 44 | if (action == ProcessRangeAction.Replace) Items.Clear(); 45 | foreach (var item in collection) 46 | { 47 | if (action == ProcessRangeAction.Remove) 48 | Items.Remove(item); 49 | else 50 | Items.Add(item); 51 | } 52 | 53 | OnPropertyChanged(new PropertyChangedEventArgs(CountString)); 54 | OnPropertyChanged(new PropertyChangedEventArgs(IndexerName)); 55 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 56 | } 57 | 58 | public void AddRange(Span collection) 59 | { 60 | ProcessRange(collection, ProcessRangeAction.Add); 61 | } 62 | 63 | public void ReplaceRange(Span collection) 64 | { 65 | ProcessRange(collection, ProcessRangeAction.Replace); 66 | } 67 | 68 | public void RemoveRange(Span collection) 69 | { 70 | ProcessRange(collection, ProcessRangeAction.Remove); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /LocalizationTracker/Utility/RepairKit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace LocalizationTracker.Utility 8 | { 9 | class RepairKit 10 | { 11 | public static void RemovePrefixs(string path) 12 | { 13 | using (ZipArchive zFile = ZipFile.Open(path, ZipArchiveMode.Update)) 14 | { 15 | foreach (var zEntry in zFile.Entries) 16 | { 17 | string text; 18 | using var stream = zEntry.Open(); 19 | using var sr = new StreamReader(stream, encoding: Encoding.UTF8, leaveOpen: true); 20 | text = sr.ReadToEnd(); 21 | 22 | text = text.Replace("", ""); 26 | text = text.Replace("xmlns:x", "xmlns"); 27 | 28 | stream.Seek(0, SeekOrigin.Begin); 29 | using StreamWriter sw = new(stream, encoding: Encoding.UTF8, leaveOpen: true); 30 | sw.Write(text); 31 | stream.SetLength(stream.Position); 32 | } 33 | } 34 | } 35 | 36 | public static void RepairPathInExportXLSX(string path) 37 | { 38 | using (ZipArchive file = ZipFile.Open(path, ZipArchiveMode.Update)) 39 | { 40 | var brokenNames = file.Entries.Where(v => !v.FullName.Replace("\\", "/").Equals(v.FullName, StringComparison.InvariantCultureIgnoreCase)).Select(v => v.FullName).ToArray(); 41 | foreach (var brokenName in brokenNames) 42 | { 43 | var entry = file.GetEntry(brokenName); 44 | var newName = brokenName.Replace("\\", "/"); 45 | var newEntry = file.CreateEntry(newName); 46 | using (var newStream = newEntry.Open()) 47 | using (var oldStream = entry.Open()) 48 | { 49 | oldStream.CopyTo(newStream); 50 | } 51 | entry.Delete(); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LocalizationTracker/Utility/StopWatchUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace LocalizationTracker 5 | { 6 | static class StopWatchUtils 7 | { 8 | /// 9 | /// Gets estimated time on compleation. 10 | /// 11 | public static TimeSpan GetEta(this Stopwatch sw, int counter, int counterGoal) 12 | { 13 | if (counter == 0) return TimeSpan.Zero; 14 | float elapsedMin = ((float)sw.ElapsedMilliseconds / 1000) / 60; 15 | float minLeft = (elapsedMin / counter) * (counterGoal - counter); //see comment a 16 | TimeSpan ret = TimeSpan.FromMinutes(minLeft); 17 | return ret; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LocalizationTracker/Utility/SystemExtentions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace LocalizationTracker 5 | { 6 | public static class SystemExtentions 7 | { 8 | public static void ForEach(this IEnumerable ienumerable, Action action) 9 | { 10 | foreach (var item in ienumerable) 11 | { 12 | action(item); 13 | } 14 | } 15 | 16 | public static bool Contains(this IEnumerable source, Predicate predicate) 17 | => source.TryFind(predicate, out var result); 18 | 19 | public static bool TryFind(this IEnumerable source, Predicate predicate, out T result) 20 | { 21 | foreach (var item in source) 22 | { 23 | if (predicate(item)) 24 | { 25 | result = item; 26 | return true; 27 | } 28 | } 29 | 30 | result = default; 31 | return false; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LocalizationTracker/Utility/WinFormsUtility.cs: -------------------------------------------------------------------------------- 1 | using LocalizationTracker.Windows; 2 | using Microsoft.Win32; 3 | using System.IO; 4 | using System.Windows; 5 | 6 | namespace LocalizationTracker.Utility 7 | { 8 | class WinFormsUtility 9 | { 10 | public static ExportParams? TryGetExportParams(Window owner) 11 | { 12 | var dialog = new ExportDialog { Owner = owner }; 13 | if (dialog.ShowDialog() == true) 14 | return dialog.ExportParams; 15 | return default; 16 | } 17 | 18 | 19 | public static bool TryGetOpenFilePath(string rootPath, string filter, out string[] selectedFiles) 20 | { 21 | var fileDialog = new OpenFileDialog() 22 | { 23 | InitialDirectory = rootPath, 24 | Filter = filter, 25 | Multiselect = true 26 | }; 27 | 28 | selectedFiles = fileDialog.ShowDialog() == true ? fileDialog.FileNames : new string[0]; 29 | return selectedFiles.Length > 0; 30 | } 31 | 32 | public static void OpenFolderAndSelectFile(string filePath) 33 | { 34 | if (!File.Exists(filePath)) 35 | { 36 | return; 37 | } 38 | 39 | // combine the arguments together 40 | // it doesn't matter if there is a space after ',' 41 | string argument = "/select, \"" + filePath + "\""; 42 | 43 | System.Diagnostics.Process.Start("explorer.exe", argument); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LocalizationTracker/ViewModel/LocaleDetailsVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Windows; 5 | using JetBrains.Annotations; 6 | using LocalizationTracker.Components; 7 | using StringsCollector.Data.Wrappers; 8 | 9 | namespace LocalizationTracker.ViewModel 10 | { 11 | public class LocaleDetailsVM 12 | { 13 | public Locale Locale { get; internal set; } 14 | 15 | public DateTimeOffset? ModificationDate { get; internal set; } 16 | 17 | [NotNull] 18 | public InlinesWrapper Text { get; internal set; } 19 | 20 | public Visibility TranslationVisibility { get; internal set; } 21 | 22 | public Locale TranslatedFrom { get; internal set; } 23 | 24 | public DateTimeOffset TranslationDate { get; internal set; } 25 | 26 | [NotNull] 27 | public InlinesWrapper OriginalText { get; internal set; } 28 | 29 | public String TranslatorComment { get; internal set; } 30 | 31 | [NotNull] 32 | public List Traits { get; internal set; } 33 | 34 | public Visibility TraitsVisibility 35 | => Traits.Count <= 0 36 | ? Visibility.Collapsed 37 | : Visibility.Visible; 38 | 39 | // designer data 40 | internal LocaleDetailsVM() 41 | { 42 | Locale = Locale.DefaultTo; 43 | ModificationDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); 44 | Text = new InlinesWrapper("Locale text"); 45 | TranslationVisibility = Visibility.Visible; 46 | TranslatedFrom = Locale.DefaultFrom; 47 | TranslationDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(5); 48 | OriginalText = new InlinesWrapper("Текст в русской локали"); 49 | TranslatorComment = ""; 50 | 51 | var finalTrait = new TraitDetailsVM("Final", "Final Text"); 52 | var relevatTrait = new TraitDetailsVM("Relevant", "Relevant Text"); 53 | Traits = new List {finalTrait, relevatTrait}; 54 | } 55 | 56 | public LocaleDetailsVM(ILocaleData localeData) 57 | { 58 | Locale = localeData.Locale; 59 | ModificationDate = localeData.ModificationDate; 60 | Text = new InlinesWrapper(localeData.Text); 61 | TranslatorComment = localeData.TranslatedComment; 62 | 63 | bool translated = localeData.TranslatedFrom != null && localeData.TranslatedFrom != Locale.Empty; 64 | if (translated) 65 | { 66 | TranslationVisibility = Visibility.Visible; 67 | TranslatedFrom = localeData.TranslatedFrom; 68 | TranslationDate = localeData.TranslationDate ?? DateTimeOffset.MinValue; 69 | OriginalText = new InlinesWrapper(localeData.OriginalText); 70 | } 71 | else 72 | { 73 | TranslationVisibility = Visibility.Collapsed; 74 | TranslatedFrom = Locale; 75 | TranslationDate = DateTimeOffset.MinValue; 76 | OriginalText = new InlinesWrapper(); 77 | } 78 | 79 | Traits = localeData.Traits? 80 | .OrderBy(or => or.ModificationDate) 81 | .Select(td => new TraitDetailsVM(td)) 82 | .ToList() ?? new List(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /LocalizationTracker/ViewModel/SpeakersChangedVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LocalizationTracker.ViewModel 9 | { 10 | public class SpeakersChangedVM 11 | { 12 | [NotNull] 13 | public string Path { get; set; } 14 | 15 | [NotNull] 16 | public string Key { get; set; } 17 | public string Status { get; set; } 18 | 19 | public string? OldSpeaker { get; set; } 20 | public string? ActualSpeaker { get; set; } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LocalizationTracker/ViewModel/StringDetailsVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Media; 7 | using System.Windows.Media.Imaging; 8 | using JetBrains.Annotations; 9 | using LocalizationTracker.Data; 10 | using static StringsCollector.Data.Unreal.UnrealStringData; 11 | 12 | namespace LocalizationTracker.ViewModel 13 | { 14 | public class StringDetailsVM 15 | { 16 | public static readonly StringDetailsVM Empty 17 | = new StringDetailsVM { CommonVisibility = Visibility.Collapsed }; 18 | 19 | public Visibility CommonVisibility { get; private set; } 20 | 21 | public Locale Source { get; } 22 | 23 | [NotNull] 24 | public string Path { get; set; } 25 | 26 | [NotNull] 27 | public string Key { get; } 28 | 29 | [NotNull] 30 | public string Comment { get; } 31 | 32 | [NotNull] 33 | public StringKind Kind { get; } 34 | 35 | [NotNull] 36 | public string Speaker { get; } 37 | 38 | [NotNull] 39 | public LocaleDetailsVM[] Locales { get; } 40 | 41 | [NotNull] 42 | public TraitDetailsVM[] StringTraits { get; } 43 | 44 | public Visibility CommentVisibility 45 | => string.IsNullOrEmpty(Comment) 46 | ? Visibility.Collapsed 47 | : Visibility.Visible; 48 | 49 | public Visibility SpeakerVisibility 50 | => string.IsNullOrEmpty(Speaker) 51 | ? Visibility.Collapsed 52 | : Visibility.Visible; 53 | 54 | public Visibility AttachmentVisibility 55 | => string.IsNullOrEmpty(AttachedImagePath) 56 | ? Visibility.Collapsed 57 | : Visibility.Visible; 58 | 59 | public Visibility TraitsVisibility 60 | => StringTraits.Length <= 0 61 | ? Visibility.Collapsed 62 | : Visibility.Visible; 63 | 64 | public Visibility KindVisibility 65 | => Kind == StringKind.Default 66 | ? Visibility.Collapsed 67 | : Visibility.Visible; 68 | 69 | public string AttachedImagePath { get; } 70 | 71 | public StringDetailsVM() 72 | { 73 | CommonVisibility = Visibility.Visible; 74 | Path = "path/to/text.json"; 75 | Key = "aaaaa-bbbbb-cccc"; 76 | Comment = "Comment"; 77 | Speaker = "Speaker"; 78 | 79 | var ruLocale = new LocaleDetailsVM(); 80 | var enLocale = new LocaleDetailsVM(); 81 | 82 | ruLocale.Locale = enLocale.TranslatedFrom; 83 | ruLocale.Text = enLocale.OriginalText; 84 | ruLocale.TranslationVisibility = Visibility.Collapsed; 85 | 86 | Locales = new[] { ruLocale, enLocale }; 87 | 88 | var importantTrait = new TraitDetailsVM("Important"); 89 | StringTraits = new[] { importantTrait }; 90 | } 91 | 92 | public StringDetailsVM(StringEntry se) 93 | { 94 | CommonVisibility = Visibility.Visible; 95 | var stringData = se.Data; 96 | Path = se.PathRelativeToStringsFolder; 97 | Source = stringData.Source; 98 | Key = stringData.Key; 99 | Comment = stringData.Comment; 100 | Kind = stringData.Kind; 101 | Speaker = stringData.Speaker; 102 | AttachedImagePath = string.IsNullOrEmpty(stringData.AttachmentPath) 103 | ? "" 104 | : System.IO.Path.GetFullPath( 105 | System.IO.Path.Combine(AppConfig.Instance.AttachmentsPath, stringData.AttachmentPath)); 106 | 107 | Locales = stringData.Languages 108 | .Select(ld => new LocaleDetailsVM(ld)) 109 | .ToArray(); 110 | 111 | StringTraits = stringData.StringTraits? 112 | .OrderBy(or => or.ModificationDate) 113 | .Select(td => new TraitDetailsVM(td)) 114 | .ToArray() ?? new TraitDetailsVM[0]; 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /LocalizationTracker/ViewModel/TraitDetailsVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using LocalizationTracker.Components; 4 | using StringsCollector.Data.Wrappers; 5 | 6 | namespace LocalizationTracker.ViewModel 7 | { 8 | public class TraitDetailsVM 9 | { 10 | public string Trait { get; } 11 | 12 | public DateTimeOffset ModificationDate { get; } 13 | 14 | public InlinesWrapper Text { get; } 15 | 16 | public Visibility TextVisibility 17 | => Text.HasAny 18 | ? Visibility.Visible 19 | : Visibility.Collapsed; 20 | 21 | // designer data 22 | public TraitDetailsVM() 23 | { 24 | Trait = "Final"; 25 | ModificationDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(2); 26 | Text = new InlinesWrapper("Final Text"); 27 | } 28 | 29 | // designer data 30 | internal TraitDetailsVM(string trait, string text = "") 31 | { 32 | Trait = trait; 33 | ModificationDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(2); 34 | Text = new InlinesWrapper(text); 35 | } 36 | 37 | public TraitDetailsVM(ITraitData traitData) 38 | { 39 | Trait = traitData.Trait; 40 | ModificationDate = traitData.ModificationDate.ToLocalTime(); 41 | Text = new InlinesWrapper(traitData.LocaleText); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /LocalizationTracker/Windows/AuthorizationWindow.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 |