diff --git a/MetadataTranslator/MTCmd/Program.cs b/MetadataTranslator/MTCmd/Program.cs index 146381e..5656c13 100644 --- a/MetadataTranslator/MTCmd/Program.cs +++ b/MetadataTranslator/MTCmd/Program.cs @@ -65,7 +65,7 @@ namespace MTCmd } catch (Exception ex) { - Console.WriteLine($"{ex}"); + Console.Error.WriteLine($"{ex}"); } }); @@ -75,29 +75,66 @@ namespace MTCmd static void Import(DataModel model, FileInfo importFile, bool overwriteMode) { + Func import = (lcid) => { + if (Path.GetExtension(importFile.Name).Equals(".csv", StringComparison.InvariantCultureIgnoreCase)) + { + model.ImportFromCsv(importFile.FullName, lcid, overwriteMode); + return true; + } + else if (Path.GetExtension(importFile.Name).Equals(".resx", StringComparison.InvariantCultureIgnoreCase)) + { + string referenceResx = $"{importFile.DirectoryName}\\{model.DefaultCulture}.resx"; + if(File.Exists(referenceResx)) + { + try + { + model.ImportFromResx(importFile.FullName, referenceResx, lcid, overwriteMode); + return true; + } + catch (NoResxMatchesException noMatch) + { + Console.Error.WriteLine(string.Format(Strings.NoResxMatches, noMatch.TranslationResx, noMatch.ReferenceResx)); + return false; + } + } + else + { + Console.Error.WriteLine(string.Format(Strings.noResxReferenceFile, importFile.FullName, referenceResx)); + return false; + } + } + else + { + Console.Error.WriteLine(Strings.invalidFileType); + return false; + } + }; + if (importFile != null) { string lcid = Path.GetFileNameWithoutExtension(importFile.Name); if (model.SetLanguageFlags(lcid, true)) { - model.ImportFromCsv(importFile.FullName, lcid, overwriteMode); - model.Update(); - Console.WriteLine(Strings.importSuccess, importFile); + if (import(lcid)) + { + model.Update(); + Console.WriteLine(Strings.importSuccess, importFile); + } } else { - Console.WriteLine(Strings.invalidLocale); + Console.Error.WriteLine(Strings.invalidLocale); } } else { - Console.WriteLine(Strings.noImportFileSpecified); + Console.Error.WriteLine(Strings.noImportFileSpecified); } } static void Export(Mode mode, DataModel model, DirectoryInfo exportFolder, string lcid) { - Action export = (path) => { if (mode == Mode.ExportResx) model.ExportToResx(path); else model.ExportToCsv(path); }; + Action export = () => { if (mode == Mode.ExportResx) model.ExportToResx(exportFolder.FullName); else model.ExportToCsv(exportFolder.FullName); }; if (exportFolder != null) { @@ -106,22 +143,22 @@ namespace MTCmd model.DeselectAllLanguages(); model.SetLanguageFlags(lcid, true, false); - export(exportFolder.FullName); + export(); Console.WriteLine(Strings.singleLocalExportSuccess, lcid, exportFolder); } - else if (model.HasTargetLanguages) + else if (model.HasTargetLanguages || mode == Mode.ExportResx) { - export(exportFolder.FullName); + export(); Console.WriteLine(Strings.exportSuccess, exportFolder); } else { - Console.WriteLine(Strings.noExportableTranslations); + Console.Error.WriteLine(Strings.noExportableTranslations); } } else { - Console.WriteLine(Strings.noExportFolderSpecified); + Console.Error.WriteLine(Strings.noExportFolderSpecified); } } } diff --git a/MetadataTranslator/MTCmd/Strings.Designer.cs b/MetadataTranslator/MTCmd/Strings.Designer.cs index a859355..92aa295 100644 --- a/MetadataTranslator/MTCmd/Strings.Designer.cs +++ b/MetadataTranslator/MTCmd/Strings.Designer.cs @@ -88,7 +88,7 @@ namespace MTCmd { } /// - /// Looks up a localized string similar to The full path and name of the comma-separated values (csv) translation file to import.. + /// Looks up a localized string similar to The full path and name of the comma-separated values (csv) or resource (resx) translation file to import.. /// internal static string ifDescription { get { @@ -106,7 +106,16 @@ namespace MTCmd { } /// - /// Looks up a localized string similar to Either the imported language matches the default locale, or the file name does not correspond to a supported locale identifier. The file name convention is <lcid>.csv.. + /// Looks up a localized string similar to Invalid translation file type. The file must be either a comma-separated values (csv) file or a resource (resx) file.. + /// + internal static string invalidFileType { + get { + return ResourceManager.GetString("invalidFileType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Either the imported language matches the default locale, or the file name does not correspond to a supported locale identifier. The file name convention is <lcid>.csv or <lcid>.resx.. /// internal static string invalidLocale { get { @@ -159,6 +168,24 @@ namespace MTCmd { } } + /// + /// Looks up a localized string similar to No translated strings found! The keys of the reference resx '{0}' and the translation resx '{1}' don't seem to match.. + /// + internal static string NoResxMatches { + get { + return ResourceManager.GetString("NoResxMatches", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to import '{0}' because the reference resx file with the default language strings was not found. In addition to the import resx, make sure the following reference resx file also exists: {1}. + /// + internal static string noResxReferenceFile { + get { + return ResourceManager.GetString("noResxReferenceFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Metadata Translator command-line app. /// diff --git a/MetadataTranslator/MTCmd/Strings.resx b/MetadataTranslator/MTCmd/Strings.resx index 52ee966..6444fed 100644 --- a/MetadataTranslator/MTCmd/Strings.resx +++ b/MetadataTranslator/MTCmd/Strings.resx @@ -127,13 +127,16 @@ Existing translations exported to: {0} - The full path and name of the comma-separated values (csv) translation file to import. + The full path and name of the comma-separated values (csv) or resource (resx) translation file to import. Translation file {0} imported successfully. + + Invalid translation file type. The file must be either a comma-separated values (csv) file or a resource (resx) file. + - Either the imported language matches the default locale, or the file name does not correspond to a supported locale identifier. The file name convention is <lcid>.csv. + Either the imported language matches the default locale, or the file name does not correspond to a supported locale identifier. The file name convention is <lcid>.csv or <lcid>.resx. A valid Windows Language Code Identifier (LCID), aka language tag, such as en-US, af-NA, and zh-CN. @@ -150,6 +153,12 @@ Use the --import-file option to specify an import file for this operation. + + No translated strings found! The keys of the reference resx '{0}' and the translation resx '{1}' don't seem to match. + + + Unable to import '{0}' because the reference resx file with the default language strings was not found. In addition to the import resx, make sure the following reference resx file also exists: {1} + Metadata Translator command-line app diff --git a/MetadataTranslator/Metadata Translator Setup/Metadata Translator Setup.vdproj b/MetadataTranslator/Metadata Translator Setup/Metadata Translator Setup.vdproj index e196446..e23fe78 100644 --- a/MetadataTranslator/Metadata Translator Setup/Metadata Translator Setup.vdproj +++ b/MetadataTranslator/Metadata Translator Setup/Metadata Translator Setup.vdproj @@ -15,30 +15,30 @@ { "Entry" { - "MsmKey" = "8:_0635D5A6F23DB1379A0FD91FF1B01507" - "OwnerKey" = "8:_7C7487CE661C4E1E5239FB4306E0749B" + "MsmKey" = "8:_15129D0347D11843939442B84751A2BC" + "OwnerKey" = "8:_1EF590E9755E072BDC7697262C8261FD" "MsmSig" = "8:_UNDEFINED" } "Entry" { - "MsmKey" = "8:_0635D5A6F23DB1379A0FD91FF1B01507" - "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_0635D5A6F23DB1379A0FD91FF1B01507" + "MsmKey" = "8:_15129D0347D11843939442B84751A2BC" "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" "MsmSig" = "8:_UNDEFINED" } "Entry" { - "MsmKey" = "8:_0635D5A6F23DB1379A0FD91FF1B01507" + "MsmKey" = "8:_15129D0347D11843939442B84751A2BC" "OwnerKey" = "8:_C48ECEE77D85A17550522737B8522D2B" "MsmSig" = "8:_UNDEFINED" } "Entry" { + "MsmKey" = "8:_15129D0347D11843939442B84751A2BC" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { "MsmKey" = "8:_19CCDCF64DCD42A423A21A106271862F" "OwnerKey" = "8:_4A5BE4840FB638200664527C5EDB50B1" "MsmSig" = "8:_UNDEFINED" @@ -70,6 +70,12 @@ "Entry" { "MsmKey" = "8:_441D4DFF8EBA539070D14AA52B77A767" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_441D4DFF8EBA539070D14AA52B77A767" "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" "MsmSig" = "8:_UNDEFINED" } @@ -81,12 +87,6 @@ } "Entry" { - "MsmKey" = "8:_441D4DFF8EBA539070D14AA52B77A767" - "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { "MsmKey" = "8:_4A5BE4840FB638200664527C5EDB50B1" "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" "MsmSig" = "8:_UNDEFINED" @@ -135,60 +135,6 @@ } "Entry" { - "MsmKey" = "8:_7C7487CE661C4E1E5239FB4306E0749B" - "OwnerKey" = "8:_C48ECEE77D85A17550522737B8522D2B" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_7C7487CE661C4E1E5239FB4306E0749B" - "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_7C7487CE661C4E1E5239FB4306E0749B" - "OwnerKey" = "8:_1EF590E9755E072BDC7697262C8261FD" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_7C7487CE661C4E1E5239FB4306E0749B" - "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_905A4AA564BAC6CC36A45AEE36EFE2FD" - "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_905A4AA564BAC6CC36A45AEE36EFE2FD" - "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_905A4AA564BAC6CC36A45AEE36EFE2FD" - "OwnerKey" = "8:_1EF590E9755E072BDC7697262C8261FD" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_905A4AA564BAC6CC36A45AEE36EFE2FD" - "OwnerKey" = "8:_7C7487CE661C4E1E5239FB4306E0749B" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_905A4AA564BAC6CC36A45AEE36EFE2FD" - "OwnerKey" = "8:_C48ECEE77D85A17550522737B8522D2B" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { "MsmKey" = "8:_9069E279B1FD50CD11B425DE3C9211AC" "OwnerKey" = "8:_B92FE44943D4DEC27E1C3FA927231844" "MsmSig" = "8:_UNDEFINED" @@ -201,26 +147,26 @@ } "Entry" { - "MsmKey" = "8:_A1F0AFE5FCA14283B33A21CAC42178E4" - "OwnerKey" = "8:_C0C5DE7473DD8B2395E58F2E339A1AF1" + "MsmKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" + "OwnerKey" = "8:_15129D0347D11843939442B84751A2BC" "MsmSig" = "8:_UNDEFINED" } "Entry" { - "MsmKey" = "8:_A1F0AFE5FCA14283B33A21CAC42178E4" - "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_A1F0AFE5FCA14283B33A21CAC42178E4" + "MsmKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" "MsmSig" = "8:_UNDEFINED" } "Entry" { - "MsmKey" = "8:_A1F0AFE5FCA14283B33A21CAC42178E4" - "OwnerKey" = "8:_905A4AA564BAC6CC36A45AEE36EFE2FD" + "MsmKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" + "OwnerKey" = "8:_C48ECEE77D85A17550522737B8522D2B" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" "MsmSig" = "8:_UNDEFINED" } "Entry" @@ -267,6 +213,30 @@ } "Entry" { + "MsmKey" = "8:_CF08BC6F349ADA91DCE542635C8FFE0B" + "OwnerKey" = "8:_EAFD1746D4EDBAE590F6668ACEC315D5" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_CF08BC6F349ADA91DCE542635C8FFE0B" + "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_CF08BC6F349ADA91DCE542635C8FFE0B" + "OwnerKey" = "8:_C0C5DE7473DD8B2395E58F2E339A1AF1" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_CF08BC6F349ADA91DCE542635C8FFE0B" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { "MsmKey" = "8:_DE34EA7F69E9468ED2359C26A753AE38" "OwnerKey" = "8:_C4ACAD76FA650ADD691588B6D2AA3BB2" "MsmSig" = "8:_UNDEFINED" @@ -279,6 +249,36 @@ } "Entry" { + "MsmKey" = "8:_EAFD1746D4EDBAE590F6668ACEC315D5" + "OwnerKey" = "8:_1EF590E9755E072BDC7697262C8261FD" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_EAFD1746D4EDBAE590F6668ACEC315D5" + "OwnerKey" = "8:_C48ECEE77D85A17550522737B8522D2B" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_EAFD1746D4EDBAE590F6668ACEC315D5" + "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_EAFD1746D4EDBAE590F6668ACEC315D5" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_EAFD1746D4EDBAE590F6668ACEC315D5" + "OwnerKey" = "8:_15129D0347D11843939442B84751A2BC" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { "MsmKey" = "8:_UNDEFINED" "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" "MsmSig" = "8:_UNDEFINED" @@ -286,6 +286,18 @@ "Entry" { "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_C48ECEE77D85A17550522737B8522D2B" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_C0C5DE7473DD8B2395E58F2E339A1AF1" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" "MsmSig" = "8:_UNDEFINED" } @@ -328,6 +340,36 @@ "Entry" { "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_15129D0347D11843939442B84751A2BC" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_EAFD1746D4EDBAE590F6668ACEC315D5" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_CF08BC6F349ADA91DCE542635C8FFE0B" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_441D4DFF8EBA539070D14AA52B77A767" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" "OwnerKey" = "8:_B92FE44943D4DEC27E1C3FA927231844" "MsmSig" = "8:_UNDEFINED" } @@ -355,48 +397,6 @@ "OwnerKey" = "8:_9069E279B1FD50CD11B425DE3C9211AC" "MsmSig" = "8:_UNDEFINED" } - "Entry" - { - "MsmKey" = "8:_UNDEFINED" - "OwnerKey" = "8:_C48ECEE77D85A17550522737B8522D2B" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_UNDEFINED" - "OwnerKey" = "8:_7C7487CE661C4E1E5239FB4306E0749B" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_UNDEFINED" - "OwnerKey" = "8:_0635D5A6F23DB1379A0FD91FF1B01507" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_UNDEFINED" - "OwnerKey" = "8:_905A4AA564BAC6CC36A45AEE36EFE2FD" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_UNDEFINED" - "OwnerKey" = "8:_C0C5DE7473DD8B2395E58F2E339A1AF1" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_UNDEFINED" - "OwnerKey" = "8:_A1F0AFE5FCA14283B33A21CAC42178E4" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_UNDEFINED" - "OwnerKey" = "8:_441D4DFF8EBA539070D14AA52B77A767" - "MsmSig" = "8:_UNDEFINED" - } } "Configurations" { @@ -454,6 +454,14 @@ "PrerequisitesLocation" = "2:1" "Url" = "8:" "ComponentsUrl" = "8:" + "Items" + { + "{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:.NETFramework,Version=v4.7.2" + { + "Name" = "8:Microsoft .NET Framework 4.7.2 (x86 and x64)" + "ProductCode" = "8:.NETFramework,Version=v4.7.2" + } + } } } } @@ -512,20 +520,20 @@ } "File" { - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_0635D5A6F23DB1379A0FD91FF1B01507" + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_15129D0347D11843939442B84751A2BC" { "AssemblyRegister" = "3:1" "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.Tabular.Json, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" + "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.Tabular, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" "ScatterAssemblies" { - "_0635D5A6F23DB1379A0FD91FF1B01507" + "_15129D0347D11843939442B84751A2BC" { - "Name" = "8:Microsoft.AnalysisServices.Tabular.Json.dll" + "Name" = "8:Microsoft.AnalysisServices.Tabular.dll" "Attributes" = "3:512" } } - "SourcePath" = "8:Microsoft.AnalysisServices.Tabular.Json.dll" + "SourcePath" = "8:Microsoft.AnalysisServices.Tabular.dll" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_E31DB11D2BA64930BBAB95E69D1A099C" @@ -800,68 +808,6 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_7C7487CE661C4E1E5239FB4306E0749B" - { - "AssemblyRegister" = "3:1" - "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.Tabular, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" - "ScatterAssemblies" - { - "_7C7487CE661C4E1E5239FB4306E0749B" - { - "Name" = "8:Microsoft.AnalysisServices.Tabular.dll" - "Attributes" = "3:512" - } - } - "SourcePath" = "8:Microsoft.AnalysisServices.Tabular.dll" - "TargetName" = "8:" - "Tag" = "8:" - "Folder" = "8:_E31DB11D2BA64930BBAB95E69D1A099C" - "Condition" = "8:" - "Transitive" = "11:FALSE" - "Vital" = "11:TRUE" - "ReadOnly" = "11:FALSE" - "Hidden" = "11:FALSE" - "System" = "11:FALSE" - "Permanent" = "11:FALSE" - "SharedLegacy" = "11:FALSE" - "PackageAs" = "3:1" - "Register" = "3:1" - "Exclude" = "11:FALSE" - "IsDependency" = "11:TRUE" - "IsolateTo" = "8:" - } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_905A4AA564BAC6CC36A45AEE36EFE2FD" - { - "AssemblyRegister" = "3:1" - "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.Core, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" - "ScatterAssemblies" - { - "_905A4AA564BAC6CC36A45AEE36EFE2FD" - { - "Name" = "8:Microsoft.AnalysisServices.Core.dll" - "Attributes" = "3:512" - } - } - "SourcePath" = "8:Microsoft.AnalysisServices.Core.dll" - "TargetName" = "8:" - "Tag" = "8:" - "Folder" = "8:_E31DB11D2BA64930BBAB95E69D1A099C" - "Condition" = "8:" - "Transitive" = "11:FALSE" - "Vital" = "11:TRUE" - "ReadOnly" = "11:FALSE" - "Hidden" = "11:FALSE" - "System" = "11:FALSE" - "Permanent" = "11:FALSE" - "SharedLegacy" = "11:FALSE" - "PackageAs" = "3:1" - "Register" = "3:1" - "Exclude" = "11:FALSE" - "IsDependency" = "11:TRUE" - "IsolateTo" = "8:" - } "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_9069E279B1FD50CD11B425DE3C9211AC" { "AssemblyRegister" = "3:1" @@ -913,20 +859,20 @@ "IsDependency" = "11:FALSE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_A1F0AFE5FCA14283B33A21CAC42178E4" + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_A915B6D2A5B737C989D5A9820C90541B" { "AssemblyRegister" = "3:1" "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.SPClient.Interfaces, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" + "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.Tabular.Json, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" "ScatterAssemblies" { - "_A1F0AFE5FCA14283B33A21CAC42178E4" + "_A915B6D2A5B737C989D5A9820C90541B" { - "Name" = "8:Microsoft.AnalysisServices.SPClient.Interfaces.dll" + "Name" = "8:Microsoft.AnalysisServices.Tabular.Json.dll" "Attributes" = "3:512" } } - "SourcePath" = "8:Microsoft.AnalysisServices.SPClient.Interfaces.dll" + "SourcePath" = "8:Microsoft.AnalysisServices.Tabular.Json.dll" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_E31DB11D2BA64930BBAB95E69D1A099C" @@ -1099,6 +1045,37 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_CF08BC6F349ADA91DCE542635C8FFE0B" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.SPClient.Interfaces, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_CF08BC6F349ADA91DCE542635C8FFE0B" + { + "Name" = "8:Microsoft.AnalysisServices.SPClient.Interfaces.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:Microsoft.AnalysisServices.SPClient.Interfaces.dll" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_E31DB11D2BA64930BBAB95E69D1A099C" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:TRUE" + "IsolateTo" = "8:" + } "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_DE34EA7F69E9468ED2359C26A753AE38" { "AssemblyRegister" = "3:1" @@ -1130,6 +1107,37 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_EAFD1746D4EDBAE590F6668ACEC315D5" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.Core, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_EAFD1746D4EDBAE590F6668ACEC315D5" + { + "Name" = "8:Microsoft.AnalysisServices.Core.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:Microsoft.AnalysisServices.Core.dll" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_E31DB11D2BA64930BBAB95E69D1A099C" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:TRUE" + "IsolateTo" = "8:" + } } "FileType" { @@ -1197,15 +1205,15 @@ { "Name" = "8:Microsoft Visual Studio" "ProductName" = "8:Metadata Translator" - "ProductCode" = "8:{B6113405-F39B-4074-ACFE-79AB32CC2CFA}" - "PackageCode" = "8:{FB893543-9B0F-4DD8-8470-D4FEF396FAE2}" + "ProductCode" = "8:{6E78ABEB-1417-473B-BA84-60B10C49BA4A}" + "PackageCode" = "8:{84CEE7B5-BB29-4386-B26C-360A47BB3ECF}" "UpgradeCode" = "8:{67E5697C-787F-4F4D-A7B8-C0C2CB4392DD}" "AspNetVersion" = "8:4.0.30319.0" "RestartWWWService" = "11:FALSE" "RemovePreviousVersions" = "11:FALSE" "DetectNewerInstalledVersion" = "11:TRUE" "InstallAllUsers" = "11:TRUE" - "ProductVersion" = "8:1.2.0" + "ProductVersion" = "8:1.3.0" "Manufacturer" = "8:Analysis Services Samples" "ARPHELPTELEPHONE" = "8:" "ARPHELPLINK" = "8:" diff --git a/MetadataTranslator/Metadata Translator/Data/DataModel.cs b/MetadataTranslator/Metadata Translator/Data/DataModel.cs index 955f728..cbc1d33 100644 --- a/MetadataTranslator/Metadata Translator/Data/DataModel.cs +++ b/MetadataTranslator/Metadata Translator/Data/DataModel.cs @@ -17,6 +17,7 @@ using Adomd = Microsoft.AnalysisServices.AdomdClient; using System.Text.RegularExpressions; using System.Security.Cryptography; using System.Resources; +using System.Collections; namespace Metadata_Translator { @@ -504,40 +505,66 @@ namespace Metadata_Translator /// public void ImportFromCsv(string filePath, string lcid, bool replaceExistingTranslations) { - try + string csvData = File.ReadAllText(filePath); + if (string.IsNullOrEmpty(csvData)) return; + + List parsedRows = new List(); + + using (TextFieldParser parser = new TextFieldParser(new StringReader(csvData))) { - string csvData = File.ReadAllText(filePath); - if (string.IsNullOrEmpty(csvData)) return; + parser.CommentTokens = new string[] { "#" }; + parser.SetDelimiters(new string[] { "," }); + parser.HasFieldsEnclosedInQuotes = true; - List parsedRows = new List(); - - using (TextFieldParser parser = new TextFieldParser(new StringReader(csvData))) + /// Skip the header row. + /// + parser.ReadFields(); + while (!parser.EndOfData) { - parser.CommentTokens = new string[] { "#" }; - parser.SetDelimiters(new string[] { "," }); - parser.HasFieldsEnclosedInQuotes = true; - - /// Skip the header row. - /// - parser.ReadFields(); - while (!parser.EndOfData) + var textFields = parser.ReadFields(); + if (textFields != null && textFields.Count() == 3) { - var textFields = parser.ReadFields(); - if (textFields != null && textFields.Count() == 3) + parsedRows.Add(new CsvRow { - parsedRows.Add(new CsvRow - { - Type = textFields[0], - Original = textFields[1], - Translation = textFields[2] - }); - } + Type = textFields[0], + Original = textFields[1], + Translation = textFields[2] + }); } } - - ApplyTranslation(lcid, parsedRows, replaceExistingTranslations); } - catch { } + + ApplyTranslation(lcid, parsedRows, replaceExistingTranslations); + } + + public void ImportFromResx(string filePath, string referencePath, string lcid, bool replaceExistingTranslations) + { + List parsedRows = new List(); + + using (ResXResourceReader defaultLocaleStrings = new ResXResourceReader(referencePath)) + using (ResXResourceSet translatedStrings = new ResXResourceSet(filePath)) + { + foreach (DictionaryEntry kvp in defaultLocaleStrings) + { + string key = kvp.Key.ToString(); + string value = kvp.Value?.ToString(); + string translation = translatedStrings.GetString(key); + if (!string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(translation)) + { + parsedRows.Add(new CsvRow + { + Type = string.Empty, + Original = value, + Translation = translation + }); + } + } + } + + if (parsedRows.Count == 0) + throw new NoResxMatchesException(filePath, referencePath); + + ApplyTranslation(lcid, parsedRows, replaceExistingTranslations); } /// @@ -548,6 +575,11 @@ namespace Metadata_Translator /// private void ApplyTranslation(string lcid, List translatedRows, bool replaceExistingTranslations) { + if (translatedRows == null || translatedRows.Count == 0) + return; + + bool hasTypeInfo = !string.IsNullOrEmpty(translatedRows[0].Type); + var allDataRows = GetAllDataRows(); if(!MatchAllRows(allDataRows, lcid, translatedRows, replaceExistingTranslations)) { @@ -558,7 +590,8 @@ namespace Metadata_Translator { var metaContainer = (MetadataObjectContainer)row.GetObject(ContainerColumnHeader); var original = row.GetValue(DefaultCulture); - var csvRow = translatedRows.Where(x => x.Type == metaContainer.TranslatedProperty.ToString() && x.Original.Equals(original)).FirstOrDefault(); + var csvRow = (hasTypeInfo)? translatedRows.Where(x => x.Type == metaContainer.TranslatedProperty.ToString() && x.Original.Equals(original)).FirstOrDefault() : + translatedRows.Where(x => x.Original.Equals(original)).FirstOrDefault(); if(csvRow != null) { row.SetValue(lcid, csvRow.Translation, replaceExistingTranslations); diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/csv versus resx.png b/MetadataTranslator/Metadata Translator/Documentation/Images/csv versus resx.png new file mode 100644 index 0000000..2398d07 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/csv versus resx.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/README.md b/MetadataTranslator/Metadata Translator/Documentation/README.md index cb8cfcd..b1e67ba 100644 --- a/MetadataTranslator/Metadata Translator/Documentation/README.md +++ b/MetadataTranslator/Metadata Translator/Documentation/README.md @@ -142,8 +142,90 @@ The following command imports German translations from a .csv file called de-DE. > > MTCmd.exe only imports one translation file at a time. To import multiple languages, run MTCmd.exe in a loop. +### Working with resx files + +While the graphical user interface of Metadata Translator only works with csv files, the command-line tool supports an additional option to work with XML resource (.resx) files as well. Resx files rely on a well-defined XML schema, including a header followed by data in name/value pairs. These files are often used for software localization. Among other things, Visual Studio provides a convenient interface for creating and maintaining resx files. And with command-line support for resx files in Metadata Translator, you can reuse these software translations by applying them to your Power BI datasets. + +#### Csv versus Resx + +Conceptually, resx files are slightly more difficult to work with than csv files because software localization and dataset localization use different approaches. In software projects structured for localization, UI elements are typically assigned an identifier, which then maps to an actual value in a resx file. For each supported language, the software project includes a separate resx file (or set of resx files). In this sense, there is no default locale. The identifiers establish the relationship or string mapping, and all supported languages are equal. + +In Power BI datasets, on the other hand, named objects don't have identifiers. They hold the strings of the default locale so that measures, relationships, and other metadata elements can use meaningful references and clients connecting without specifying a locale on the connection string get meaningful information, such as human-readable table and column names. Moreover, there is no reliable way to associate the metadata objects in a dataset with a persisted unique identifier. For this reason, Metadata Translator strongly favors csv files because csv permits a direct relationship between the strings of the default locale and the translated strings of an additional locale without the help of a relationship base on an identifier. + +The following figure illustrates the differences between csv and resx files for a dataset with a default locale of *en-US* and an additional locale of *it-IT*. Exporting the strings into a csv file produces a single it-IT.csv file containing both the original (default locale) strings and the translated strings. On the other hand, exporting the strings into resx produces two files, one for each locale with a globally unique identifier establishing the relationship between them. If you lost the en-US.resx file, for example, you practically also lost the Italian translations because you no longer have a mapping to the strings of the default locale. + +![csv versus resx](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/csv%20versus%20resx.png) + +#### Exporting into resx files + +Exporting translations from a dataset into resx files is very similar to exporting into csv files. Just specify the ExportResx as the mode of operation. For example, the following command exports all translations into resx files from an AdventureWorks dataset hosted in Power BI into a folder called ExportedTranslations: + +`MTCmd -cs "powerbi://api.powerbi.com/v1.0/myorg/AdventureWorksSource;initial catalog=AdventureWorks" -ef C:\ExportedTranslations -m ExportResx` + +> Note +> +> The specified export folder must exist prior to running the command. MTCmd.exe does not create the export folder. However, if any files exist in the export folder, MTCmd.exe might overwrite them without warning. MTCmd.exe exports each locale into a separate .csv file based on the locale identifier (LCID), including the default locale. + +#### Repeatedly exporting into resx files + +It is important to note that Metadata Translator generates new globally unique identifiers (GUIDs) every time you export translations from a dataset. There is no reliable way for Metadata Translator to persist previously generated GUIDs with their corresponding metadata objects in the model. It is therefore not recommended to export translations into an existing set of resx files. + +For illustration, imagine the following scenario: + +1. You export the translations from a dataset that supports the locales en-US, it-IT, and es-ES. The default locale is en-US. +2. Metadata Translator creates to following files in the export folder: en-US.resx, it-IT.resx, and es-ES.resx. All three files use the same GUIDs to establish the string relationships. In other words, these three files from a translation set of resx files. +3. You export the Italian translations again by using the -lcid parameter (-lcid it-IT). You use the same export folder. Metadata Translator overwrites the en-US.resx and it-IT.resx files. +4. The existing es-ES.resx file is now orphaned because the GUIDs in the new en-US.resx file, representing the default locale of this dataset, no longer match. +5. To bring this translation set of resx files back into a consistent state, you must export the full set of locales again. + +> Note +> +> To avoid orphaned resx files, it is a good idea to always export all locales together. If you must export individual languages, make sure you export them into a separate (empty) folder to avoid possibly overwriting an existing resx file of the default locale. + +#### Importing translations from resx files + +Importing translations from resx files is practically identical to importing from csv files. Simply specify the resx import file by using the --import-file (-if) parameter. Metadata Translator detects the import mode based in the .resx file name extension. For more details, refer to the section "Importing translations" earlier in this document. + +The following command imports German translations from a resx file called de-DE.resx into an AdventureWorks dataset hosted in Power BI, overwriting any existing German strings in the dataset: + +`MTCmd -cs "powerbi://api.powerbi.com/v1.0/myorg/AdventureWorksSource;initial catalog=AdventureWorks" -if C:\Translations\de-DE.resx -m Overwrite` + +> Note +> +> For a resx import operation to succeed, Metadata Translator requires the resx file of the dataset's default locale to be located in the same folder as the translation resx file. For example, if the above command attempts to import German translations into a dataset with the default locale of en-US, you must have a matching en-US.resx file in the same folder as the de-DE.resx file. + +#### Importing translations from external resx files + +There is no real difference between resx files generated by Metadata Translator vs. other tools. The GUIDs that Metadata Translator generates merely provide a convenient way to establish unique relationships between strings, but the Name values don't have to be GUIDs necessarily. Metadata Translator can use any kind of name to find a matching translation as long as the name is unique. The resx XML schema enforces uniqueness for the Name property. + +Metadata Translator performs the following steps during a resx import operation: + +1. Iterate over all the name/value pairs from the resx file of the default locale. +2. Use the name to lookup the corresponding value string in the translation resx file. +3. Use the value strings from the default and translated locales to construct a set of string pairs dynamically. +4. Use the same logic as if the string pairs were from a csv file to apply the translations to the dataset. + + + +## CSV Format for translation files + +Metadata Translator translation files use a straightforward schema with only three columns: Type,Original,Translation. Do not delete or add columns or change the column ordering because Metadata Translator will not process translation files that don't follow this schema. Refer to the following table for more details. + +| Column | Description | +| ----------- | ------------------------------------------------------------ | +| Type | An optional value to specify the metadata property type to which the translation should apply when Metadata Translator switches to case-sensitive string matching.
Supported values are:
Caption
Description
DisplayFolder
If you specify an invalid property type, the translation might be ignored. Leave this column blank if you want to apply a translation to all property types across the board. | +| Original | A string in the default locale of the dataset. Metadata Translator compares the caption, description, or display folder name of a metadata object with this string using case-sensitive string matching to determine if the translation applies to this object. Make sure the captions, descriptions, and display folder names of your metadata objects match the strings in the Original column exactly. | +| Translation | A translated string in the locale of the csv file, as identified by the csv file name, such as de-DE.csv. | + +> Note +> +> Ideally, strings in the Original column of a translation file fully match of the captions, descriptions, and display folder names of the metadata objects, including their ordering. However, you can also maintain a global set of translation files that apply to multiple different datasets. In this case, Metadata Translators uses case-sensitive string matching to determine which translation to apply. If you compile translation files by using a separate tool and don't know the property type, leave the Type column empty, but make sure the column exists. + ## Additional features -Metadata Translator v1.2 does not support editing the default language strings because the external tools integration feature of Power BI Desktop does not support these operations yet. +Metadata Translator v1.3 does not support editing the default language strings because the external tools integration feature of Power BI Desktop does not support these operations yet. + +Metadata Translator v1.3 does not yet support translating synonyms for Power BI Q&A, mainly because Power BI Q&A currently only supports answering natural language queries asked in English. Although there is a preview available for Spanish, the current support policy is English only (see [Supported languages and countries/regions for Power BI - Power BI | Microsoft Docs](https://docs.microsoft.com/power-bi/fundamentals/supported-languages-countries-regions#whats-translated)). For additional feature requests, create a new item under [Issues](https://github.com/microsoft/Analysis-Services/issues). + diff --git a/MetadataTranslator/Metadata Translator/Helpers/NoResxMatchesException.cs b/MetadataTranslator/Metadata Translator/Helpers/NoResxMatchesException.cs new file mode 100644 index 0000000..c1f2c0f --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Helpers/NoResxMatchesException.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public class NoResxMatchesException : Exception + { + public string ReferenceResx { get; set; } + public string TranslationResx { get; set; } + + public NoResxMatchesException(string referenceResx, string translationResx) + { + ReferenceResx = referenceResx; + TranslationResx = translationResx; + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Metadata Translator.csproj b/MetadataTranslator/Metadata Translator/Metadata Translator.csproj index 25bc4d2..e3aed8f 100644 --- a/MetadataTranslator/Metadata Translator/Metadata Translator.csproj +++ b/MetadataTranslator/Metadata Translator/Metadata Translator.csproj @@ -104,6 +104,7 @@ MSBuild:Compile Designer + ConnectionStringInput.xaml diff --git a/MetadataTranslator/Metadata Translator/Properties/AssemblyInfo.cs b/MetadataTranslator/Metadata Translator/Properties/AssemblyInfo.cs index a261f9d..a5cef7f 100644 --- a/MetadataTranslator/Metadata Translator/Properties/AssemblyInfo.cs +++ b/MetadataTranslator/Metadata Translator/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.2.0.0")] -[assembly: AssemblyFileVersion("1.2.0.0")] +[assembly: AssemblyVersion("1.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] diff --git a/MetadataTranslator/Metadata Translator/UI/ImportExportPanel.xaml.cs b/MetadataTranslator/Metadata Translator/UI/ImportExportPanel.xaml.cs index 8828182..d15f812 100644 --- a/MetadataTranslator/Metadata Translator/UI/ImportExportPanel.xaml.cs +++ b/MetadataTranslator/Metadata Translator/UI/ImportExportPanel.xaml.cs @@ -48,14 +48,19 @@ namespace Metadata_Translator if (openFileDialog1.ShowDialog() == true) { - using (new Hourglass()) + using (new Hourglass()) { foreach (string filePath in openFileDialog1.FileNames) { string lcid = Path.GetFileNameWithoutExtension(filePath); mainWnd.AddColumn(lcid); - mainWnd.DataModel?.ImportFromCsv(filePath, lcid, mainWnd.OverwriteTranslation); + + try + { + mainWnd.DataModel?.ImportFromCsv(filePath, lcid, mainWnd.OverwriteTranslation); + } + catch { } } } }