diff --git a/BestPracticeRules/BPARules.json b/BestPracticeRules/BPARules.json new file mode 100644 index 0000000..8549c95 --- /dev/null +++ b/BestPracticeRules/BPARules.json @@ -0,0 +1,560 @@ +[ + { + "ID": "AVOID_FLOATING_POINT_DATA_TYPES", + "Name": "[Performance] Do not use floating point data types", + "Category": "Performance", + "Description": "The \"Double\" floating point data type should be avoided, as it can result in unpredictable roundoff errors and decreased performance in certain scenarios. Use \"Int64\" or \"Decimal\" where appropriate (but note that \"Decimal\" is limited to 4 digits after the decimal sign).", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "DataType = \"Double\"", + "FixExpression": "DataType = DataType.Decimal", + "CompatibilityLevel": 1200 + }, + { + "ID": "ISAVAILABLEINMDX_FALSE_NONATTRIBUTE_COLUMNS", + "Name": "[Performance] Set IsAvailableInMdx to false on non-attribute columns", + "Category": "Performance", + "Description": "To speed up processing time and conserve memory after processing, attribute hierarchies should not be built for columns that are never used for slicing by MDX clients. In other words, all hidden columns that are not used as a Sort By Column or referenced in user hierarchies should have their IsAvailableInMdx property set to false.\r\nReference: https://blog.crossjoin.co.uk/2018/07/02/isavailableinmdx-ssas-tabular/", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "IsAvailableInMDX\r\nand\r\n\n(IsHidden or Table.IsHidden)\r\nand\r\n\nnot UsedInSortBy.Any() \r\nand\r\n\nnot UsedInHierarchies.Any()", + "FixExpression": "IsAvailableInMDX = false", + "CompatibilityLevel": 1200 + }, + { + "ID": "AVOID_BI-DIRECTIONAL_RELATIONSHIPS_AGAINST_HIGH-CARDINALITY_COLUMNS", + "Name": "[Performance] Avoid bi-directional relationships against high-cardinality columns", + "Category": "Performance", + "Description": "For best performance, it is recommended to avoid using bi-directional relationships against high-cardinality columns. In order to run this rule, you must first run the script shown here: https://www.elegantbi.com/post/vertipaqintabulareditor", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "UsedInRelationships.Any(CrossFilteringBehavior == CrossFilteringBehavior.BothDirections)\n\nand\n\nConvert.ToInt32(GetAnnotation(\"Vertipaq_Cardinality\")) > 1000000", + "CompatibilityLevel": 1200 + }, + { + "ID": "REDUCE_USAGE_OF_LONG-LENGTH_COLUMNS_WITH_HIGH_CARDINALITY", + "Name": "[Performance] Reduce usage of long-length columns with high cardinality", + "Category": "Performance", + "Description": "It is best to avoid lengthy text columns. This is especially true if the column has many unique values. These types of columns can cause longer processing times, bloated model sizes, as well as slower user queries. Long length is defined as more than 100 characters.", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "Convert.ToInt32(GetAnnotation(\"LongLengthRowCount\")) > 500000", + "CompatibilityLevel": 1200 + }, + { + "ID": "SPLIT_DATE_AND_TIME", + "Name": "[Performance] Split date and time", + "Category": "Performance", + "Description": "This rule finds datetime columns that have values not at midnight. To maximize performance, the time element should be split from date element (or the time component should be rounded to midnight as this will reduce column cardinality).\r\nReference: https://www.sqlbi.com/articles/separate-date-and-time-in-powerpivot-and-bism-tabular/", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "Convert.ToInt32(GetAnnotation(\"DateTimeWithHourMinSec\")) > 0", + "CompatibilityLevel": 1200 + }, + { + "ID": "LARGE_TABLES_SHOULD_BE_PARTITIONED", + "Name": "[Performance] Large tables should be partitioned", + "Category": "Performance", + "Description": "Large tables should be partitioned in order to optimize processing. In order for this rule to run properly, you must run the script shown here: https://www.elegantbi.com/post/vertipaqintabulareditor", + "Severity": 2, + "Scope": "Table", + "Expression": "Convert.ToInt64(GetAnnotation(\"Vertipaq_RowCount\")) > 25000000\r\nand\r\nPartitions.Count = 1", + "CompatibilityLevel": 1200 + }, + { + "ID": "REDUCE_USAGE_OF_CALCULATED_COLUMNS_THAT_USE_THE_RELATED_FUNCTION", + "Name": "[Performance] Reduce usage of calculated columns that use the RELATED function", + "Category": "Performance", + "Description": "Calculated columns do not compress as well as data columns and may cause longer processing times. As such, calculated columns should be avoided if possible. One scenario where they may be easier to avoid is if they use the RELATED function.\r\nReference: https://www.sqlbi.com/articles/storage-differences-between-calculated-columns-and-calculated-tables/", + "Severity": 2, + "Scope": "CalculatedColumn", + "Expression": "RegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)RELATED\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)RELATED\\(\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "SNOWFLAKE_SCHEMA_ARCHITECTURE", + "Name": "[Performance] Consider a star-schema instead of a snowflake architecture", + "Category": "Performance", + "Description": "Generally speaking, a star-schema is the optimal architecture for tabular models. That being the case, there are valid cases to use a snowflake approach. Please check your model and consider moving to a star-schema architecture.\r\nReference: https://docs.microsoft.com/en-us/power-bi/guidance/star-schema", + "Severity": 2, + "Scope": "Table, CalculatedTable", + "Expression": "UsedInRelationships.Any(current.Name == FromTable.Name)\r\nand\r\nUsedInRelationships.Any(current.Name == ToTable.Name)", + "CompatibilityLevel": 1200 + }, + { + "ID": "MODEL_SHOULD_HAVE_A_DATE_TABLE", + "Name": "[Performance] Model should have a date table", + "Category": "Performance", + "Description": "Generally speaking, models should generally have a date table. Models that do not have a date table generally are not taking advantage of features such as time intelligence or may not have a properly structured architecture.", + "Severity": 2, + "Scope": "Model", + "Expression": "Tables.Any(DataCategory == \"Time\" && Columns.Any(IsKey == true && DataType == \"DateTime\")) == false", + "CompatibilityLevel": 1200 + }, + { + "ID": "DATE/CALENDAR_TABLES_SHOULD_BE_MARKED_AS_A_DATE_TABLE", + "Name": "[Performance] Date/calendar tables should be marked as a date table", + "Category": "Performance", + "Description": "This rule looks for tables that contain the words 'date' or 'calendar' as they should likely be marked as a date table.\r\nReference: https://docs.microsoft.com/en-us/power-bi/transform-model/desktop-date-tables", + "Severity": 2, + "Scope": "Table, CalculatedTable", + "Expression": "(Name.ToUpper().Contains(\"DATE\") or Name.ToUpper().Contains(\"CALENDAR\"))\n\nand\n\n(\nDataCategory <> \"Time\"\n\nor\n\nColumns.Any(IsKey == true && DataType == \"DateTime\") == false\n)", + "CompatibilityLevel": 1200 + }, + { + "ID": "REMOVE_AUTO-DATE_TABLE", + "Name": "[Performance] Remove auto-date table", + "Category": "Performance", + "Description": "Avoid using auto-date tables. Make sure to turn off auto-date table in the settings in Power BI Desktop. This will save memory resources. \r\nReference: https://www.youtube.com/watch?v=xu3uDEHtCrg", + "Severity": 2, + "Scope": "Table, CalculatedTable", + "Expression": "ObjectTypeName == \"Calculated Table\"\n\r\nand\r\n\n(\nName.StartsWith(\"DateTableTemplate_\") \n\nor \n\nName.StartsWith(\"LocalDateTable_\")\n)", + "CompatibilityLevel": 1200 + }, + { + "ID": "AVOID_EXCESSIVE_BI-DIRECTIONAL_OR_MANY-TO-MANY_RELATIONSHIPS", + "Name": "[Performance] Avoid excessive bi-directional or many-to-many relationships", + "Category": "Performance", + "Description": "Limit use of b-di and many-to-many relationships. This rule flags the model if more than 30% of relationships are bi-di or many-to-many.\r\nReference: https://www.sqlbi.com/articles/bidirectional-relationships-and-ambiguity-in-dax/", + "Severity": 2, + "Scope": "Model", + "Expression": "(\r\n\nRelationships.Where(CrossFilteringBehavior == CrossFilteringBehavior.BothDirections).Count()\r\n\n+\r\n\nRelationships.Where(FromCardinality.ToString() == \"Many\" && ToCardinality.ToString() == \"Many\").Count()\r\n\n)\r\n\n\n/\r\n\n\nMath.Max(Convert.ToDecimal(Relationships.Count)\n\n,1)> 0.3", + "CompatibilityLevel": 1200 + }, + { + "ID": "LIMIT_ROW_LEVEL_SECURITY_(RLS)_LOGIC", + "Name": "[Performance] Limit row level security (RLS) logic", + "Category": "Performance", + "Description": "Try to simplify the DAX used for row level security. Usage of the functions within this rule can likely be offloaded to the upstream systems (data warehouse).", + "Severity": 2, + "Scope": "Table, CalculatedTable", + "Expression": "RowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)RIGHT\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)RIGHT\\(\"))\r\nor\r\nRowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)LEFT\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)LEFT\\(\"))\r\nor\r\nRowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)UPPER\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)UPPER\\(\"))\r\nor\r\nRowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)LOWER\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)LOWER\\(\"))\r\nor\r\nRowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)FIND\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)FIND\\(\"))", + "CompatibilityLevel": 1200 + }, + { + "ID": "MODEL_USING_DIRECT_QUERY_AND_NO_AGGREGATIONS", + "Name": "[Performance] Consider using aggregations if using Direct Query in Power BI", + "Category": "Performance", + "Description": "If using Direct Query in Power BI Premium, you may want to consider using aggregations in order to boost performance.\r\nReference: https://docs.microsoft.com/en-us/power-bi/transform-model/desktop-aggregations", + "Severity": 1, + "Scope": "Model", + "Expression": "Tables.Any(ObjectTypeName == \"Table (DirectQuery)\")\r\nand\r\n\n\nAllColumns.Any(AlternateOf != null) == false\r\nand \r\nDefaultPowerBIDataSourceVersion.ToString() == \"PowerBI_V3\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "MINIMIZE_POWER_QUERY_TRANSFORMATIONS", + "Name": "[Performance] Minimize Power Query transformations", + "Category": "Performance", + "Description": "Minimize Power Query transformations in order to improve model processing performance. It is a best practice to offload these transformations to the data warehouse if possible. Also, please check whether query folding is occurring within your model. Please reference the article below for more information on query folding.\r\nReference: https://docs.microsoft.com/en-us/power-query/power-query-folding", + "Severity": 2, + "Scope": "Partition", + "Expression": "\nSourceType.ToString() = \"M\"\r\nand\r\n(\r\nQuery.Contains(\"Table.Combine(\")\r\nor\r\n\nQuery.Contains(\"Table.Join(\")\r\nor\r\n\nQuery.Contains(\"Table.NestedJoin(\")\r\nor\r\nQuery.Contains(\"Table.AddColumn(\")\r\nor\r\nQuery.Contains(\"Table.Group(\")\r\nor\r\nQuery.Contains(\"Table.Sort(\")\r\nor\r\nQuery.Contains(\"Table.Pivot(\")\r\nor\r\nQuery.Contains(\"Table.Unpivot(\")\r\nor\r\nQuery.Contains(\"Table.UnpivotOtherColumns(\")\r\nor\r\nQuery.Contains(\"Table.Distinct(\")\r\nor\r\nQuery.Contains(\"[Query=\"\"SELECT\")\r\nor\r\nQuery.Contains(\"Value.NativeQuery\")\r\nor\r\nQuery.Contains(\"OleDb.Query\")\r\nor\r\nQuery.Contains(\"Odbc.Query\")\r\n)", + "CompatibilityLevel": 1200 + }, + { + "ID": "UNPIVOT_PIVOTED_(MONTH)_DATA", + "Name": "[Performance] Unpivot pivoted (month) data", + "Category": "Performance", + "Description": "Avoid using pivoted data in your tables. This rule checks specifically for pivoted data by month.\r\nReference: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 2, + "Scope": "Table, CalculatedTable", + "Expression": "Columns.Any(Name.ToUpper().Contains(\"JAN\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"FEB\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"MAR\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"APR\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"MAY\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"JUN\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))", + "CompatibilityLevel": 1200 + }, + { + "ID": "MANY-TO-MANY_RELATIONSHIPS_SHOULD_BE_SINGLE-DIRECTION", + "Name": "[Performance] Many-to-many relationships should be single-direction", + "Category": "Performance", + "Severity": 2, + "Scope": "Relationship", + "Expression": "FromCardinality == \"Many\"\nand\nToCardinality == \"Many\"\nand\nCrossFilteringBehavior == \"BothDirections\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "REDUCE_USAGE_OF_CALCULATED_TABLES", + "Name": "[Performance] Reduce usage of calculated tables", + "Category": "Performance", + "Description": "Migrate calculated table logic to your data warehouse. Reliance on calculated tables will lead to technical debt and potential misalignments if you have multiple models on your platform.", + "Severity": 2, + "Scope": "CalculatedTable", + "Expression": "1=1", + "CompatibilityLevel": 1200 + }, + { + "ID": "REMOVE_REDUNDANT_COLUMNS_IN_RELATED_TABLES", + "Name": "[Performance] Remove redundant columns in related tables", + "Category": "Performance", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "UsedInRelationships.Any() == false \r\nand\r\nModel.AllColumns.Any(Name == current.Name and Table.Name != current.Table.Name and Table.UsedInRelationships.Any(FromTable.Name == current.Table.Name))", + "CompatibilityLevel": 1200 + }, + { + "ID": "MEASURES_USING_TIME_INTELLIGENCE_AND_MODEL_IS_USING_DIRECT_QUERY", + "Name": "[Performance] Measures using time intelligence and model is using Direct Query", + "Category": "Performance", + "Description": "At present, time intelligence functions are known to not perform as well when using Direct Query. If you are having performance issues, you may want to try alternative solutions such as adding columns in the fact table that show previous year or previous month data.", + "Severity": 2, + "Scope": "Measure, CalculationItem", + "Expression": "Model.Tables.Any(ObjectTypeName == \"Table (DirectQuery)\")\r\nand\r\n(\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"CLOSINGBALANCEMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"CLOSINGBALANCEQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"CLOSINGBALANCEYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATEADD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESBETWEEN(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESINPERIOD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESMTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESQTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESYTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"ENDOFMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"ENDOFQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"ENDOFYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"FIRSTDATE(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"FIRSTNONBLANK(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"FIRSTNONBLANKVALUE(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"LASTDATE(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"LASTNONBLANK(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"LASTNONBLANKVALUE(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"NEXTDAY(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"NEXTMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"NEXTQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"NEXTYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"OPENINGBALANCEMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"OPENINGBALANCEQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"OPENINGBALANCEYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PARALLELPERIOD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PREVIOUSDAY(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PREVIOUSMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PREVIOUSQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PREVIOUSYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"SAMEPERIODLASTYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"STARTOFMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"STARTOFQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"STARTOFYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"TOTALMTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"TOTALQTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"TOTALYTD(\")\r\n)", + "CompatibilityLevel": 1200 + }, + { + "ID": "REDUCE_NUMBER_OF_CALCULATED_COLUMNS", + "Name": "[Performance] Reduce number of calculated columns", + "Category": "Performance", + "Description": "Calculated columns do not compress as well as data columns so they take up more memory. They also slow down processing times for both the table as well as process recalc. Offload calculated column logic to your data warehouse and turn these calculated columns into data columns.\r\nReference: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 2, + "Scope": "Model", + "Expression": "AllColumns.Where(Type.ToString() == \"Calculated\").Count() > 5", + "CompatibilityLevel": 1200 + }, + { + "ID": "CHECK_IF_BI-DIRECTIONAL_AND_MANY-TO-MANY_RELATIONSHIPS_ARE_VALID", + "Name": "[Performance] Check if bi-directional and many-to-many relationships are valid", + "Category": "Performance", + "Description": "Bi-directional and many-to-many relationships may cause performance degradation or even have unintended consequences. Make sure to check these specific relationships to ensure they are working as designed and are actually necessary.\r\nReference: https://www.sqlbi.com/articles/bidirectional-relationships-and-ambiguity-in-dax/", + "Severity": 1, + "Scope": "Relationship", + "Expression": "FromCardinality.ToString() = \"Many\" and ToCardinality.ToString() = \"Many\"\r\nor\r\nCrossFilteringBehavior == CrossFilteringBehavior.BothDirections", + "CompatibilityLevel": 1200 + }, + { + "ID": "CHECK_IF_DYNAMIC_ROW_LEVEL_SECURITY_(RLS)__IS_NECESSARY", + "Name": "[Performance] Check if dynamic row level security (RLS) is necessary", + "Category": "Performance", + "Description": "Usage of dynamic row level security (RLS) can add memory and performance overhead. Please research the pros/cons of using it.\r\nReference: https://docs.microsoft.com/en-us/power-bi/admin/service-admin-rls", + "Severity": 1, + "Scope": "Table, CalculatedTable", + "Expression": "RegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)USERNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)USERNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)USERPRINCIPALNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)USERPRINCIPALNAME\\(\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "DAX_COLUMNS_FULLY_QUALIFIED", + "Name": "[DAX Expressions] Column references should be fully qualified", + "Category": "DAX Expressions", + "Description": "Using fully qualified column references makes it easier to distinguish between column and measure references, and also helps avoid certain errors. When referencing a column in DAX, first specify the table name, then specify the column name in square brackets.\r\nReference: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 3, + "Scope": "Measure, KPI, CalculationItem", + "Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))", + "CompatibilityLevel": 1200 + }, + { + "ID": "DAX_MEASURES_UNQUALIFIED", + "Name": "[DAX Expressions] Measure references should be unqualified", + "Category": "DAX Expressions", + "Description": "Using unqualified measure references makes it easier to distinguish between column and measure references, and also helps avoid certain errors. When referencing a measure using DAX, do not specify the table name. Use only the measure name in square brackets.\r\nReference: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 3, + "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI, CalculationItem", + "Expression": "DependsOn.Any(Key.ObjectType = \"Measure\" and Value.Any(FullyQualified))", + "CompatibilityLevel": 1200 + }, + { + "ID": "AVOID_DUPLICATE_MEASURES", + "Name": "[DAX Expressions] No two measures should have the same definition", + "Category": "DAX Expressions", + "Severity": 2, + "Scope": "Measure", + "Expression": "Model.AllMeasures.Any(Expression.Replace(\" \",\"\").Replace(\"\\n\",\"\").Replace(\"\\r\",\"\").Replace(\"\\t\",\"\") = outerIt.Expression.Replace(\" \",\"\").Replace(\"\\n\",\"\").Replace(\"\\r\",\"\").Replace(\"\\t\",\"\") and it <> outerIt)", + "CompatibilityLevel": 1200 + }, + { + "ID": "USE_THE_TREATAS_FUNCTION_INSTEAD_OF_INTERSECT", + "Name": "[DAX Expressions] Use the TREATAS function instead of INTERSECT for virtual relationships", + "Category": "DAX Expressions", + "Description": "The TREATAS function is more efficient and provides better performance than the INTERSECT function when used in virutal relationships.\r\nReference: https://www.sqlbi.com/articles/propagate-filters-using-treatas-in-dax/", + "Severity": 2, + "Scope": "Measure, CalculationItem", + "Expression": "RegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)INTERSECT\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)INTERSECT\\(\")", + "CompatibilityLevel": 1400 + }, + { + "ID": "USE_THE_DIVIDE_FUNCTION_FOR_DIVISION", + "Name": "[DAX Expressions] Use the DIVIDE function for division", + "Category": "DAX Expressions", + "Description": "Use the DIVIDE function instead of using \"/\". The DIVIDE function resolves divide-by-zero cases. As such, it is recommended to use to avoid errors. \r\nReference: https://docs.microsoft.com/en-us/power-bi/guidance/dax-divide-function-operator", + "Severity": 2, + "Scope": "Measure, CalculatedColumn", + "Expression": "Expression.Replace(\" \",\"\").Contains(\"]/\")\n\r\nor\r\n\nExpression.Replace(\" \",\"\").Contains(\"])/\")\n", + "CompatibilityLevel": 1200 + }, + { + "ID": "AVOID_USING_THE_IFERROR_FUNCTION", + "Name": "[DAX Expressions] Avoid using the IFERROR function", + "Category": "DAX Expressions", + "Description": "Avoid using the IFERROR function as it may cause performance degradation. If you are concerned about a divide-by-zero error, use the DIVIDE function as it naturally resolves such errors as blank (or you can customize what should be shown in case of such an error).\r\nReference: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 2, + "Scope": "Measure, CalculatedColumn", + "Expression": "RegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)IFERROR\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)IFERROR\\(\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "MEASURES_SHOULD_NOT_BE_DIRECT_REFERENCES_OF_OTHER_MEASURES", + "Name": "[DAX Expressions] Measures should not be direct references of other measures", + "Category": "DAX Expressions", + "Description": "This rule identifies measures which are simply a reference to another measure. As an example, consider a model with two measures: [MeasureA] and [MeasureB]. This rule would be triggered for MeasureB if MeasureB's DAX was MeasureB:=[MeasureA]. Such duplicative measures should be removed.", + "Severity": 2, + "Scope": "Measure", + "Expression": "Model.AllMeasures.Any(DaxObjectName == current.Expression)", + "CompatibilityLevel": 1200 + }, + { + "ID": "DATA_COLUMNS_MUST_HAVE_A_SOURCE_COLUMN", + "Name": "[Error Prevention] Data columns must have a source column", + "Category": "Error Prevention", + "Description": "Data columns must have a source column. A data column without a source column will cause an error when processing the model.", + "Severity": 3, + "Scope": "DataColumn", + "Expression": "string.IsNullOrWhitespace(SourceColumn)", + "CompatibilityLevel": 1200 + }, + { + "ID": "CALCULATED_COLUMNS_MUST_HAVE_AN_EXPRESSION", + "Name": "[Error Prevention] Calculated columns must have an expression", + "Category": "Error Prevention", + "Description": "It is recommended not to use calculated columns. However, if you do, they must have a DAX expression.", + "Severity": 3, + "Scope": "CalculatedColumn", + "Expression": "string.IsNullOrWhiteSpace(Expression)", + "CompatibilityLevel": 1200 + }, + { + "ID": "UNNECESSARY_COLUMNS", + "Name": "[Maintenance] Remove unnecessary columns", + "Category": "Maintenance", + "Description": "Hidden columns that are not referenced by any DAX expressions, relationships, hierarchy levels or Sort By-properties should be removed.", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "(IsHidden or Table.IsHidden)\n\nand ReferencedBy.Count = 0 \n\nand (not UsedInRelationships.Any())\n\nand (not UsedInSortBy.Any())\n\nand (not UsedInHierarchies.Any())\n\nand (not Table.RowLevelSecurity.Any(\n it <> null and \n it.IndexOf(\"[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0\n))\n\nand (not Model.Roles.Any(RowLevelSecurity.Any(\n it <> null and \n (\n it.IndexOf(current.Table.Name + \"[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0 or\n it.IndexOf(\"'\" + current.Table.Name + \"'[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0\n )\n)))", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "UNNECESSARY_MEASURES", + "Name": "[Maintenance] Remove unnecessary measures", + "Category": "Maintenance", + "Description": "Hidden measures that are not referenced by any DAX expressions should be removed for maintainability", + "Severity": 2, + "Scope": "Measure", + "Expression": "(Table.IsHidden or IsHidden) and ReferencedBy.Count = 0", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "REMOVE_DATA_SOURCES_NOT_REFERENCED_BY_ANY_PARTITIONS", + "Name": "[Maintenance] Remove data sources not referenced by any partitions", + "Category": "Maintenance", + "Description": "Data sources which are not referenced by any partitions may be removed.", + "Severity": 1, + "Scope": "ProviderDataSource, StructuredDataSource", + "Expression": "UsedByPartitions.Count() == 0", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "REMOVE_ROLES_WITH_NO_MEMBERS", + "Name": "[Maintenance] Remove roles with no members", + "Category": "Maintenance", + "Description": "May remove roles with no members.", + "Severity": 1, + "Scope": "ModelRole", + "Expression": "Members.Count() == 0", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "ENSURE_TABLES_HAVE_RELATIONSHIPS", + "Name": "[Maintenance] Ensure tables have relationships", + "Category": "Maintenance", + "Description": "This rule highlights tables which are not connected to any other table in the model with a relationship.", + "Severity": 1, + "Scope": "Table, CalculatedTable", + "Expression": "UsedInRelationships.Count() == 0", + "CompatibilityLevel": 1200 + }, + { + "ID": "OBJECTS_WITH_NO_DESCRIPTION", + "Name": "[Maintenance] Objects with no description", + "Category": "Maintenance", + "Description": "Add descriptions to objects. These descriptions are shown on hover within the Field List in Power BI Desktop. Additionally, you can leverage these descriptions to create an automated data dictionary (see link below).\r\nReference: https://www.elegantbi.com/post/datadictionary", + "Severity": 1, + "Scope": "Table, Measure, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup, CalculationItem", + "Expression": "string.IsNullOrWhitespace(Description)", + "CompatibilityLevel": 1200 + }, + { + "ID": "PARTITION_NAME_SHOULD_MATCH_TABLE_NAME_FOR_SINGLE_PARTITION_TABLES", + "Name": "[Naming Conventions] Partition name should match table name for single partition tables", + "Category": "Naming Conventions", + "Description": "Tables with just one partition should match their table and partition names.Tables with more than one partition should have each partition name starting with the table name.", + "Severity": 1, + "Scope": "Table", + "Expression": "(Partitions.Count = 1 and Partitions[0].Name <> Name)", + "CompatibilityLevel": 1200 + }, + { + "ID": "SPECIAL_CHARS_IN_OBJECT_NAMES", + "Name": "[Naming Conventions] Object names must not contain special characters", + "Category": "Naming Conventions", + "Description": "Tabs, line breaks, etc.", + "Severity": 2, + "Scope": "Model, Table, Measure, Hierarchy, Perspective, Partition, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup, CalculationItem", + "Expression": "Name.IndexOf(char(9)) > -1 or\nName.IndexOf(char(10)) > -1 or\nName.IndexOf(char(13)) > -1", + "CompatibilityLevel": 1200 + }, + { + "ID": "FORMAT_FLAG_COLUMNS_AS_YES/NO_VALUE_STRINGS", + "Name": "[Formatting] Format flag columns as Yes/No value strings", + "Category": "Formatting", + "Description": "Flags must be properly formatted as Yes/No as this is easier to read than using 0/1 integer values.", + "Severity": 1, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "(\nName.StartsWith(\"Is\") and \nDataType = \"Int64\" and \nnot (IsHidden or Table.IsHidden)\n) \r\nor\r\n\n(\nName.EndsWith(\" Flag\") and \nDataType <> \"String\" and \nnot (IsHidden or Table.IsHidden)\n)", + "CompatibilityLevel": 1200 + }, + { + "ID": "OBJECTS_SHOULD_NOT_START_OR_END_WITH_A_SPACE", + "Name": "[Formatting] Objects should not start or end with a space", + "Category": "Formatting", + "Description": "Objects should not start or end with a space", + "Severity": 3, + "Scope": "Model, Table, Measure, Hierarchy, Perspective, Partition, DataColumn, CalculatedColumn", + "Expression": "Name.StartsWith(\" \") or Name.EndsWith(\" \")", + "CompatibilityLevel": 1200 + }, + { + "ID": "DATECOLUMN_FORMATSTRING", + "Name": "[Formatting] Provide format string for \"Date\" columns", + "Category": "Formatting", + "Description": "Columns of type \"DateTime\" that have \"Month\" in their names should be formatted as \"mm/dd/yyyy\".", + "Severity": 1, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "Name.IndexOf(\"Date\", \"OrdinalIgnoreCase\") >= 0 and DataType = \"DateTime\" and FormatString <> \"mm/dd/yyyy\"", + "FixExpression": "FormatString = \"mm/dd/yyyy\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "MONTHCOLUMN_FORMATSTRING", + "Name": "[Formatting] Provide format string for \"Month\" columns", + "Category": "Formatting", + "Description": "Columns of type \"DateTime\" that have \"Month\" in their names should be formatted as \"MMMM yyyy\".", + "Severity": 1, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "Name.IndexOf(\"Month\", \"OrdinalIgnoreCase\") >= 0 and DataType = \"DateTime\" and FormatString <> \"MMMM yyyy\"", + "FixExpression": "FormatString = \"MMMM yyyy\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "PROVIDE_FORMAT_STRING_FOR_MEASURES", + "Name": "[Formatting] Provide format string for measures", + "Category": "Formatting", + "Description": "Visible measures should have their format string property assigned", + "Severity": 3, + "Scope": "Measure", + "Expression": "not IsHidden \r\nand not Table.IsHidden \r\nand string.IsNullOrWhitespace(FormatString)", + "CompatibilityLevel": 1200 + }, + { + "ID": "NUMERIC_COLUMN_SUMMARIZE_BY", + "Name": "[Formatting] Do not summarize numeric columns", + "Category": "Formatting", + "Description": "Numeric columns (integer, decimal, double) should have their SummarizeBy property set to \"None\" to avoid accidental summation in Power BI (create measures instead).", + "Severity": 3, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "(DataType = \"Int64\" or DataType=\"Decimal\" or DataType=\"Double\")\nand SummarizeBy <> \"None\"\nand not (IsHidden or Table.IsHidden)", + "FixExpression": "SummarizeBy = AggregateFunction.None", + "CompatibilityLevel": 1200 + }, + { + "ID": "PERCENTAGE_FORMATTING", + "Name": "[Formatting] Percentages should be formatted with thousands separators and 1 decimal", + "Category": "Formatting", + "Severity": 2, + "Scope": "Measure", + "Expression": "FormatString.Contains(\"%\") and FormatString <> \"#,0.0%;-#,0.0%;#,0.0%\"", + "FixExpression": "FormatString = \"#,0.0%;-#,0.0%;#,0.0%\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "INTEGER_FORMATTING", + "Name": "[Formatting] Whole numbers should be formatted with thousands separators and no decimals", + "Category": "Formatting", + "Severity": 2, + "Scope": "Measure", + "Expression": "not FormatString.Contains(\"$\") and not FormatString.Contains(\"%\") and not (FormatString = \"#,0\" or FormatString = \"#,0.0\")", + "FixExpression": "FormatString = \"#,0\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "RELATIONSHIP_COLUMNS_SHOULD_BE_OF_INTEGER_DATA_TYPE", + "Name": "[Formatting] Relationship columns should be of integer data type", + "Category": "Formatting", + "Description": "It is a best practice for relationship columns to be of integer data type. This applies not only to data warehousing but data modeling as well.", + "Severity": 1, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "UsedInRelationships.Any()\n\nand \n\nDataType != DataType.Int64", + "CompatibilityLevel": 1200 + }, + { + "ID": "ADD_DATA_CATEGORY_FOR_COLUMNS", + "Name": "[Formatting] Add data category for columns", + "Category": "Formatting", + "Description": "Add Data Category property for appropriate columns. \r\nReference: https://docs.microsoft.com/en-us/power-bi/transform-model/desktop-data-categorization", + "Severity": 1, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "Name == \"Country\" \r\nor \r\n\nName == \"Continent\"\n \r\nor ((\nName == \"Latitude\" \n or \nName == \"Longitude\") and (DataType == DataType.Decimal or DataType == DataType.Double))", + "CompatibilityLevel": 1200 + }, + { + "ID": "HIDE_FOREIGN_KEYS", + "Name": "[Formatting] Hide foreign keys", + "Category": "Formatting", + "Description": "Foreign keys should always be hidden.", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "UsedInRelationships.Any(FromColumn.Name == current.Name and FromCardinality == \"Many\")\nand\r\n\nIsHidden == false", + "FixExpression": "IsHidden = true", + "CompatibilityLevel": 1200 + }, + { + "ID": "MARK_PRIMARY_KEYS", + "Name": "[Formatting] Mark primary keys", + "Category": "Formatting", + "Description": "Set the 'Key' property to 'True' for primary key columns within the column properties.", + "Severity": 1, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "UsedInRelationships.Any(ToTable.Name == current.Table.Name and ToColumn.Name == current.Name and ToCardinality == \"One\")\r\n\nand\r\n\nIsKey == false\r\nand\r\ncurrent.Table.DataCategory != \"Time\"", + "FixExpression": "IsKey = true", + "CompatibilityLevel": 1200 + }, + { + "ID": "HIDE_FACT_TABLE_COLUMNS", + "Name": "[Formatting] Hide fact table columns", + "Category": "Formatting", + "Description": "It is a best practice to hide fact table columns that are used for aggregation in measures.", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "(ReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"COUNT(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"COUNTBLANK(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"SUM(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"AVERAGE(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"VALUES(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"DISTINCT(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"DISTINCTCOUNT(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"MIN(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"MAX(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"COUNTA(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"AVERAGEA(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"MAXA(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"MINA(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\n)\nand IsHidden == false\nand (DataType == \"Int64\" || DataType == \"Decimal\" || DataType == \"Double\")", + "FixExpression": "IsHidden = true", + "CompatibilityLevel": 1200 + }, + { + "ID": "FIRST_LETTER_OF_OBJECTS_MUST_BE_CAPITALIZED", + "Name": "[Formatting] First letter of objects must be capitalized", + "Category": "Formatting", + "Severity": 1, + "Scope": "Table, Measure, Hierarchy, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup", + "Expression": "Name.Substring(0,1).ToUpper() != Name.Substring(0,1)", + "CompatibilityLevel": 1200 + }, + { + "ID": "MONTH_(AS_A_STRING)_MUST_BE_SORTED", + "Name": "[Formatting] Month (as a string) must be sorted", + "Category": "Formatting", + "Description": "This rule highlights month columns which are strings and are not sorted. If left unsorted, they will sort alphabetically (i.e. April, August...). Make sure to sort such columns so that they sort properly (January, February, March...).", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "Name.ToUpper().Contains(\"MONTH\")\r\nand\r\n! Name.ToUpper().Contains(\"MONTHS\") \r\nand \r\n\n\nDataType == DataType.String \r\nand \r\nSortByColumn == null", + "CompatibilityLevel": 1200 + } +] \ No newline at end of file diff --git a/BestPracticeRules/LoadBPARules.cs b/BestPracticeRules/LoadBPARules.cs new file mode 100644 index 0000000..912d160 --- /dev/null +++ b/BestPracticeRules/LoadBPARules.cs @@ -0,0 +1,23 @@ +System.Net.WebClient w = new System.Net.WebClient(); + +string path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData); +string downloadLoc = path+@"\TabularEditor\BPARules.json"; +string url = "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/BestPracticeRules/BPARules.json"; +string dlMessage = "Downloaded BPARules.json. Please restart Tabular Editor."; + +if (System.IO.File.Exists(downloadLoc)) +{ + if (System.Windows.Forms.MessageBox.Show("Would you like to overwrite the existing BPARules.json file?","Overwrite Existing BPA Rules",System.Windows.Forms.MessageBoxButtons.YesNo,System.Windows.Forms.MessageBoxIcon.Warning) == System.Windows.Forms.DialogResult.Yes) + { + System.Windows.Forms.MessageBox.Show(dlMessage,"Overwrite Existing BPA Rules",System.Windows.Forms.MessageBoxButtons.OK,System.Windows.Forms.MessageBoxIcon.Information); + } + else + { + System.Windows.Forms.MessageBox.Show("Did not download BPARules.json.","Overwrite Existing BPA Rules",System.Windows.Forms.MessageBoxButtons.OK,System.Windows.Forms.MessageBoxIcon.Information); + } +} +else +{ + System.Windows.Forms.MessageBox.Show(dlMessage,"Download BPA Rules",System.Windows.Forms.MessageBoxButtons.OK,System.Windows.Forms.MessageBoxIcon.Information); + w.DownloadFile(url, downloadLoc); +} \ No newline at end of file diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md new file mode 100644 index 0000000..bc9e10d --- /dev/null +++ b/BestPracticeRules/README.md @@ -0,0 +1,69 @@ +# Best Practice Rules + +Make sure to also check out the [PowerBI.com blog post](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance/ "PowerBI.com blog post") on this topic! + +Also, check out this [post](https://www.elegantbi.com/post/bestpracticerulesavings "Best Practice Rule Savings") for quantifying the savings of following specific Best Practice Rules. + +## Purpose + +Running this collection of rules inside [Tabular Editor](https://tabulareditor.com/ "Tabular Editor")'s [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer") will inform you of potential issues to fix or improvements to be made with regard to performance optimization and model design. + +## Feedback + +We would love to hear feedback on how this tool has helped your organization. Please email feedback to: pbibestpractice@microsoft.com. + +If you find any issues or have any requests for new rules, please [submit an issue](https://github.com/microsoft/Analysis-Services/issues "submit an issue") within this repository. Just prefix the issue with "BPARules" to make it easier to track. + +## Setup (automated) + +Following these steps will automatically load the Best Practice Rules into your local Tabular Editor. Note that this will overwrite the existing BPARules.json file (if you are already have one). + +1. Open [Tabular Editor](https://tabulareditor.com/ "Tabular Editor"). +2. Connect to a model. +3. Run the following code in the Advanced Scripting window. + + System.Net.WebClient w = new System.Net.WebClient(); + + string path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData); + string url = "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/BestPracticeRules/BPARules.json"; + string downloadLoc = path+@"\TabularEditor\BPARules.json"; + + w.DownloadFile(url, downloadLoc); + +4. Close and reopen [Tabular Editor](https://tabulareditor.com/ "Tabular Editor"). +5. Connect to a model. +6. Select 'Tools' from the File menu and select 'Best Practice Analyzer'. +7. Click the Refresh icon (in blue). + +## Setup (manual) + +1. Download the BPARules.json file from GitHub (in this repository). +2. Within the Start Menu, type %localappdata% and click Enter. +3. Navigate to the 'TabularEditor' folder. +4. Copy the rules file (.json) and paste it into the TabularEditor folder.* +5. Open [Tabular Editor](https://tabulareditor.com/ "Tabular Editor") and connect to your model. +6. Select 'Tools' from the File menu and select 'Best Practice Analyzer'. +7. Click the Refresh icon (in blue). + +## Notes + +* The following rules require running an additional script before running the Best Practice Analyzer + + * Avoid bi-directional relationships against high-cardinality columns * + * Large tables should be partitioned * + * Reduce usage of long-length columns with high cardinality *^ + * Split date and time *** + + *These rules use [Vertipaq Analyzer](https://www.sqlbi.com/tools/vertipaq-analyzer/) data. There are 2 methods to load this data into Tabular Editor: + + 1. Load Vertipaq Analyzer data directly from a server ([instructions](https://www.elegantbi.com/post/vertipaqintabulareditor)) ([script](https://github.com/m-kovalsky/Tabular/blob/master/VertipaqAnnotations.cs)). + + 2. Load Vertipaq Analyzer data from .vpax file ([instructions](https://www.elegantbi.com/post/vpaxtotabulareditor)) ([script](https://github.com/m-kovalsky/Tabular/blob/master/VpaxToTabularEditor.cs)). + + ^Run this [script](https://github.com/m-kovalsky/Tabular/blob/master/BestPracticeRule_LongLengthColumns.cs "script") while live-connected to the model. + + ***Run this [script](https://github.com/m-kovalsky/Tabular/blob/master/BestPracticeRule_SplitDateAndTime.cs "script") while live-connected to the model. + +## Requirements + +[Tabular Editor](https://tabulareditor.com/ "Tabular Editor") version 2.12.1 or higher. diff --git a/MetadataTranslator/MTCmd/App.config b/MetadataTranslator/MTCmd/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/MetadataTranslator/MTCmd/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MetadataTranslator/MTCmd/MTCmd.csproj b/MetadataTranslator/MTCmd/MTCmd.csproj new file mode 100644 index 0000000..b032f39 --- /dev/null +++ b/MetadataTranslator/MTCmd/MTCmd.csproj @@ -0,0 +1,94 @@ + + + + + Debug + AnyCPU + {0B0EA3E1-79CC-4674-A763-9C40925DEE31} + Exe + MTCmd + MTCmd + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + icon.ico + + + + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + ..\packages\System.CommandLine.2.0.0-beta1.20574.7\lib\netstandard2.0\System.CommandLine.dll + + + + ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + + + + + + + + + + + + True + True + Strings.resx + + + + + + + + + {cb7d493c-b67e-4438-b304-efe5d418addf} + Metadata Translator + + + + + ResXFileCodeGenerator + Strings.Designer.cs + + + + + + + \ No newline at end of file diff --git a/MetadataTranslator/MTCmd/Program.cs b/MetadataTranslator/MTCmd/Program.cs new file mode 100644 index 0000000..4b6dfe2 --- /dev/null +++ b/MetadataTranslator/MTCmd/Program.cs @@ -0,0 +1,169 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using Metadata_Translator; + +namespace MTCmd +{ + public enum Mode + { + Export, + Import, + Overwrite, + ExportResx + } + + class Program + { + static int Main(string[] args) + { + /// + /// Create a root command with the required options. + /// + var rootCommand = new RootCommand(Strings.rootCmdDescription) + { + new Option( + new string[]{ "--connection-string", "-cs" }, Strings.csDescription){ IsRequired = true }, + new Option( + new string[]{ "--mode", "-m" }, + getDefaultValue: () => Mode.Export, + description: Strings.modeDescription), + new Option( + new string[]{ "--export-folder", "-ef" }, Strings.efDescription).ExistingOnly(), + new Option( + new string[]{ "--import-file", "-if" }, Strings.ifDescription).ExistingOnly(), + new Option( + new string[]{ "--locale-id", "-lcid" }, Strings.lcidDescription), + new Option( + new string[]{ "--fallback-mode", "-fm" }, Strings.fallbackDescription), + new Option( + new string[]{ "--key-prefix", "-kp" }, Strings.keyPrefixDescription) + }; + + + // Note that the parameters of the handler method are matched according to the names of the options + rootCommand.Handler = CommandHandler.Create((connectionString, mode, exportFolder, importFile, localeId, fallbackMode, keyPrefix) => + { + try + { + DataModel model = DataModel.Connect(connectionString); + model.InitializeLanguages(); + + switch (mode) + { + case Mode.Export: + case Mode.ExportResx: + Export(mode, model, exportFolder, localeId, keyPrefix); + break; + case Mode.Import: + Import(model, importFile, false, fallbackMode); + break; + case Mode.Overwrite: + Import(model, importFile, true, fallbackMode); + break; + default: + break; + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"{ex}"); + } + }); + + // Parse the incoming args and invoke the handler + return rootCommand.InvokeAsync(args).Result; + } + + static void Import(DataModel model, FileInfo importFile, bool overwriteMode, bool fallbackDefault) + { + Func import = (lcid) => { + if (Path.GetExtension(importFile.Name).Equals(".csv", StringComparison.InvariantCultureIgnoreCase)) + { + model.ImportFromCsv(importFile.FullName, lcid, overwriteMode, fallbackDefault); + 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, fallbackDefault); + 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)) + { + if (import(lcid)) + { + model.Update(); + Console.WriteLine(Strings.importSuccess, importFile); + } + } + else + { + Console.Error.WriteLine(Strings.invalidLocale); + } + } + else + { + Console.Error.WriteLine(Strings.noImportFileSpecified); + } + } + + static void Export(Mode mode, DataModel model, DirectoryInfo exportFolder, string lcid, string keyPrefix) + { + Action export = () => { if (mode == Mode.ExportResx) model.ExportToResx(exportFolder.FullName, keyPrefix); else model.ExportToCsv(exportFolder.FullName); }; + + if (exportFolder != null) + { + if (!string.IsNullOrEmpty(lcid)) + { + model.DeselectAllLanguages(); + model.SetLanguageFlags(lcid, true, false); + + export(); + Console.WriteLine(Strings.singleLocalExportSuccess, lcid, exportFolder); + } + else if (model.HasTargetLanguages || mode == Mode.ExportResx) + { + export(); + Console.WriteLine(Strings.exportSuccess, exportFolder); + } + else + { + Console.Error.WriteLine(Strings.noExportableTranslations); + } + } + else + { + Console.Error.WriteLine(Strings.noExportFolderSpecified); + } + } + } +} diff --git a/MetadataTranslator/MTCmd/Properties/AssemblyInfo.cs b/MetadataTranslator/MTCmd/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..09eb94f --- /dev/null +++ b/MetadataTranslator/MTCmd/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MTCmd")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MTCmd")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("0b0ea3e1-79cc-4674-a763-9c40925dee31")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// 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")] diff --git a/MetadataTranslator/MTCmd/Strings.Designer.cs b/MetadataTranslator/MTCmd/Strings.Designer.cs new file mode 100644 index 0000000..b2da225 --- /dev/null +++ b/MetadataTranslator/MTCmd/Strings.Designer.cs @@ -0,0 +1,225 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MTCmd { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MTCmd.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The connection string to connect to the Azure Analysis Services (Azure AS) or SQL Server Analysis Services (SSAS) data model, or Power BI Premium dataset. + /// + internal static string csDescription { + get { + return ResourceManager.GetString("csDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The full path to the export folder.. + /// + internal static string efDescription { + get { + return ResourceManager.GetString("efDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Existing translations exported to: {0}. + /// + internal static string exportSuccess { + get { + return ResourceManager.GetString("exportSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In Import mode, determines if the string from the default locale should be applied to the translated locale if the translated string is missing or cannot be found. The default setting is false, which leaves the translated string empty in the translated locale.. + /// + internal static string fallbackDescription { + get { + return ResourceManager.GetString("fallbackDescription", resourceCulture); + } + } + + /// + /// 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 { + return ResourceManager.GetString("ifDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Translation file {0} imported successfully.. + /// + internal static string importSuccess { + get { + return ResourceManager.GetString("importSuccess", resourceCulture); + } + } + + /// + /// 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 { + return ResourceManager.GetString("invalidLocale", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In ExportResx mode, influences how Metadata Translator generates the names of the key/value pairs. By default, a random GUID is generated as the name. If a key prefix is specified, then Metadata Translator generates keys based on the translated property types and the metadata object names.. + /// + internal static string keyPrefixDescription { + get { + return ResourceManager.GetString("keyPrefixDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A valid Windows Language Code Identifier (LCID), aka language tag, such as en-US, af-NA, and zh-CN.. + /// + internal static string lcidDescription { + get { + return ResourceManager.GetString("lcidDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The mode of the command-line operation. + /// + internal static string modeDescription { + get { + return ResourceManager.GetString("modeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This dataset has no translations.. + /// + internal static string noExportableTranslations { + get { + return ResourceManager.GetString("noExportableTranslations", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the --export-folder option to specify an export folder for this operation.. + /// + internal static string noExportFolderSpecified { + get { + return ResourceManager.GetString("noExportFolderSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the --import-file option to specify an import file for this operation.. + /// + internal static string noImportFileSpecified { + get { + return ResourceManager.GetString("noImportFileSpecified", resourceCulture); + } + } + + /// + /// 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. + /// + internal static string rootCmdDescription { + get { + return ResourceManager.GetString("rootCmdDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Translations for '{0}' exported to: {1}. + /// + internal static string singleLocalExportSuccess { + get { + return ResourceManager.GetString("singleLocalExportSuccess", resourceCulture); + } + } + } +} diff --git a/MetadataTranslator/MTCmd/Strings.resx b/MetadataTranslator/MTCmd/Strings.resx new file mode 100644 index 0000000..0edbd39 --- /dev/null +++ b/MetadataTranslator/MTCmd/Strings.resx @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The connection string to connect to the Azure Analysis Services (Azure AS) or SQL Server Analysis Services (SSAS) data model, or Power BI Premium dataset + + + The full path to the export folder. + + + Existing translations exported to: {0} + + + In Import mode, determines if the string from the default locale should be applied to the translated locale if the translated string is missing or cannot be found. The default setting is false, which leaves the translated string empty in the translated locale. + + + 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 or <lcid>.resx. + + + In ExportResx mode, influences how Metadata Translator generates the names of the key/value pairs. By default, a random GUID is generated as the name. If a key prefix is specified, then Metadata Translator generates keys based on the translated property types and the metadata object names. + + + A valid Windows Language Code Identifier (LCID), aka language tag, such as en-US, af-NA, and zh-CN. + + + The mode of the command-line operation + + + This dataset has no translations. + + + Use the --export-folder option to specify an export folder for this operation. + + + 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 + + + Translations for '{0}' exported to: {1} + + \ No newline at end of file diff --git a/MetadataTranslator/MTCmd/icon.ico b/MetadataTranslator/MTCmd/icon.ico new file mode 100644 index 0000000..e4317ce Binary files /dev/null and b/MetadataTranslator/MTCmd/icon.ico differ diff --git a/MetadataTranslator/MTCmd/packages.config b/MetadataTranslator/MTCmd/packages.config new file mode 100644 index 0000000..c558207 --- /dev/null +++ b/MetadataTranslator/MTCmd/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator Setup/Metadata Translator Setup.vdproj b/MetadataTranslator/Metadata Translator Setup/Metadata Translator Setup.vdproj new file mode 100644 index 0000000..e23fe78 --- /dev/null +++ b/MetadataTranslator/Metadata Translator Setup/Metadata Translator Setup.vdproj @@ -0,0 +1,1786 @@ +"DeployProject" +{ +"VSVersion" = "3:800" +"ProjectType" = "8:{978C614F-708E-4E1A-B201-565925725DBA}" +"IsWebType" = "8:FALSE" +"ProjectName" = "8:Metadata Translator Setup" +"LanguageId" = "3:1033" +"CodePage" = "3:1252" +"UILanguageId" = "3:1033" +"SccProjectName" = "8:" +"SccLocalPath" = "8:" +"SccAuxPath" = "8:" +"SccProvider" = "8:" + "Hierarchy" + { + "Entry" + { + "MsmKey" = "8:_15129D0347D11843939442B84751A2BC" + "OwnerKey" = "8:_1EF590E9755E072BDC7697262C8261FD" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_15129D0347D11843939442B84751A2BC" + "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "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" + } + "Entry" + { + "MsmKey" = "8:_19CCDCF64DCD42A423A21A106271862F" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_1EF590E9755E072BDC7697262C8261FD" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_2F1DB028FE734DBD9E77D9DCA191FAD1" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_441D4DFF8EBA539070D14AA52B77A767" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_441D4DFF8EBA539070D14AA52B77A767" + "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_441D4DFF8EBA539070D14AA52B77A767" + "OwnerKey" = "8:_1EF590E9755E072BDC7697262C8261FD" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_4A5BE4840FB638200664527C5EDB50B1" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_4DD4143C26C54926A2B59FBC551F160A" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_569CA11E5F0BB01E824CD0E8609DC800" + "OwnerKey" = "8:_19CCDCF64DCD42A423A21A106271862F" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_569CA11E5F0BB01E824CD0E8609DC800" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_579B668A1BF613B39A613E3676204554" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_579B668A1BF613B39A613E3676204554" + "OwnerKey" = "8:_19CCDCF64DCD42A423A21A106271862F" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_64EA80D7EB179AD7DF6F334604D63D4C" + "OwnerKey" = "8:_B92FE44943D4DEC27E1C3FA927231844" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_9069E279B1FD50CD11B425DE3C9211AC" + "OwnerKey" = "8:_B92FE44943D4DEC27E1C3FA927231844" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_A00C4C4DA66740A385AE811FF37BAA77" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" + "OwnerKey" = "8:_15129D0347D11843939442B84751A2BC" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" + "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" + "OwnerKey" = "8:_C48ECEE77D85A17550522737B8522D2B" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_A915B6D2A5B737C989D5A9820C90541B" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_B92FE44943D4DEC27E1C3FA927231844" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_B92FE44943D4DEC27E1C3FA927231844" + "OwnerKey" = "8:_4A5BE4840FB638200664527C5EDB50B1" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_C0C5DE7473DD8B2395E58F2E339A1AF1" + "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_C48ECEE77D85A17550522737B8522D2B" + "OwnerKey" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_C4ACAD76FA650ADD691588B6D2AA3BB2" + "OwnerKey" = "8:_B92FE44943D4DEC27E1C3FA927231844" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_C67DA394422F5927664B4F5D40B66A64" + "OwnerKey" = "8:_19CCDCF64DCD42A423A21A106271862F" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_C67DA394422F5927664B4F5D40B66A64" + "OwnerKey" = "8:_2CE91AB829584CF99B8C35A6E138B5FF" + "MsmSig" = "8:_UNDEFINED" + } + "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" + } + "Entry" + { + "MsmKey" = "8:_DE34EA7F69E9468ED2359C26A753AE38" + "OwnerKey" = "8:_B92FE44943D4DEC27E1C3FA927231844" + "MsmSig" = "8:_UNDEFINED" + } + "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" + } + "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" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_4A5BE4840FB638200664527C5EDB50B1" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_19CCDCF64DCD42A423A21A106271862F" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_569CA11E5F0BB01E824CD0E8609DC800" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_C67DA394422F5927664B4F5D40B66A64" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_579B668A1BF613B39A613E3676204554" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_1EF590E9755E072BDC7697262C8261FD" + "MsmSig" = "8:_UNDEFINED" + } + "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" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_64EA80D7EB179AD7DF6F334604D63D4C" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_C4ACAD76FA650ADD691588B6D2AA3BB2" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_DE34EA7F69E9468ED2359C26A753AE38" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_9069E279B1FD50CD11B425DE3C9211AC" + "MsmSig" = "8:_UNDEFINED" + } + } + "Configurations" + { + "Debug" + { + "DisplayName" = "8:Debug" + "IsDebugOnly" = "11:TRUE" + "IsReleaseOnly" = "11:FALSE" + "OutputFilename" = "8:Debug\\Metadata Translator Setup.msi" + "PackageFilesAs" = "3:2" + "PackageFileSize" = "3:-2147483648" + "CabType" = "3:1" + "Compression" = "3:2" + "SignOutput" = "11:FALSE" + "CertificateFile" = "8:" + "PrivateKeyFile" = "8:" + "TimeStampServer" = "8:" + "InstallerBootstrapper" = "3:2" + "BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}" + { + "Enabled" = "11:TRUE" + "PromptEnabled" = "11:TRUE" + "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" + } + } + } + } + "Release" + { + "DisplayName" = "8:Release" + "IsDebugOnly" = "11:FALSE" + "IsReleaseOnly" = "11:TRUE" + "OutputFilename" = "8:Release\\Metadata Translator x86.msi" + "PackageFilesAs" = "3:2" + "PackageFileSize" = "3:-2147483648" + "CabType" = "3:1" + "Compression" = "3:2" + "SignOutput" = "11:FALSE" + "CertificateFile" = "8:" + "PrivateKeyFile" = "8:" + "TimeStampServer" = "8:" + "InstallerBootstrapper" = "3:2" + "BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}" + { + "Enabled" = "11:TRUE" + "PromptEnabled" = "11:TRUE" + "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" + } + } + } + } + } + "Deployable" + { + "CustomAction" + { + "{4AA51A2D-7D85-4A59-BA75-B0809FC8B380}:_2C34E48AE6954790A87B87C83C3B6141" + { + "Name" = "8:Primary output from Metadata Translator (Active)" + "Condition" = "8:" + "Object" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "FileType" = "3:2" + "InstallAction" = "3:1" + "Arguments" = "8:" + "EntryPoint" = "8:" + "Sequence" = "3:1" + "Identifier" = "8:_5FA2B0FF_8A4A_482C_9229_1712B3A4403F" + "InstallerClass" = "11:TRUE" + "CustomActionData" = "8:" + } + "{4AA51A2D-7D85-4A59-BA75-B0809FC8B380}:_E30CC67BF42A4042BECEC05C4996DAB7" + { + "Name" = "8:Primary output from Metadata Translator (Active)" + "Condition" = "8:" + "Object" = "8:_5EE4AF833E364CBAB977AFCB8CD5E99D" + "FileType" = "3:2" + "InstallAction" = "3:4" + "Arguments" = "8:" + "EntryPoint" = "8:" + "Sequence" = "3:1" + "Identifier" = "8:_5E0EBF64_2328_425A_8AFC_EAC7F277C7F1" + "InstallerClass" = "11:TRUE" + "CustomActionData" = "8:" + } + } + "DefaultFeature" + { + "Name" = "8:DefaultFeature" + "Title" = "8:" + "Description" = "8:" + } + "ExternalPersistence" + { + "LaunchCondition" + { + "{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_8AA2B2E962F244F1B61E6A889F9C872D" + { + "Name" = "8:.NET Framework" + "Message" = "8:[VSDNETMSG]" + "FrameworkVersion" = "8:.NETFramework,Version=v4.7.2" + "AllowLaterVersions" = "11:FALSE" + "InstallUrl" = "8:http://go.microsoft.com/fwlink/?LinkId=863262" + } + } + } + "File" + { + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_15129D0347D11843939442B84751A2BC" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.Tabular, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_15129D0347D11843939442B84751A2BC" + { + "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}:_19CCDCF64DCD42A423A21A106271862F" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_19CCDCF64DCD42A423A21A106271862F" + { + "Name" = "8:System.Memory.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.Memory.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}:_1EF590E9755E072BDC7697262C8261FD" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:Metadata Translator, Version=1.2.0.0, Culture=neutral, PublicKeyToken=0e7f9fd02ef157fe, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_1EF590E9755E072BDC7697262C8261FD" + { + "Name" = "8:Metadata Translator.exe" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:Metadata Translator.exe" + "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:" + } + "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_2F1DB028FE734DBD9E77D9DCA191FAD1" + { + "SourcePath" = "8:..\\Metadata Translator\\metadata-translator.pbitool.json" + "TargetName" = "8:metadata-translator.pbitool.json" + "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:FALSE" + "IsolateTo" = "8:" + } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_441D4DFF8EBA539070D14AA52B77A767" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" + "ScatterAssemblies" + { + "_441D4DFF8EBA539070D14AA52B77A767" + { + "Name" = "8:System.Net.Http.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.Net.Http.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}:_4A5BE4840FB638200664527C5EDB50B1" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:System.CommandLine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_4A5BE4840FB638200664527C5EDB50B1" + { + "Name" = "8:System.CommandLine.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.CommandLine.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:" + } + "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_4DD4143C26C54926A2B59FBC551F160A" + { + "SourcePath" = "8:..\\Metadata Translator\\Resources\\supportedlanguages.json" + "TargetName" = "8:supportedlanguages.json" + "Tag" = "8:" + "Folder" = "8:_B58920A79D7F4ECFB278D11291BCEBBB" + "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:FALSE" + "IsolateTo" = "8:" + } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_569CA11E5F0BB01E824CD0E8609DC800" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_569CA11E5F0BB01E824CD0E8609DC800" + { + "Name" = "8:System.Runtime.CompilerServices.Unsafe.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.Runtime.CompilerServices.Unsafe.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}:_579B668A1BF613B39A613E3676204554" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_579B668A1BF613B39A613E3676204554" + { + "Name" = "8:System.Buffers.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.Buffers.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}:_64EA80D7EB179AD7DF6F334604D63D4C" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:TRUE" + "AssemblyAsmDisplayName" = "8:System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_64EA80D7EB179AD7DF6F334604D63D4C" + { + "Name" = "8:System.Net.Http.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.Net.Http.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" + "AssemblyIsInGAC" = "11:TRUE" + "AssemblyAsmDisplayName" = "8:System.Diagnostics.Tracing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_9069E279B1FD50CD11B425DE3C9211AC" + { + "Name" = "8:System.Diagnostics.Tracing.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.Diagnostics.Tracing.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:" + } + "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_A00C4C4DA66740A385AE811FF37BAA77" + { + "SourcePath" = "8:..\\Metadata Translator\\icon.ico" + "TargetName" = "8:icon.ico" + "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:FALSE" + "IsolateTo" = "8:" + } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_A915B6D2A5B737C989D5A9820C90541B" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.Tabular.Json, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_A915B6D2A5B737C989D5A9820C90541B" + { + "Name" = "8:Microsoft.AnalysisServices.Tabular.Json.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:Microsoft.AnalysisServices.Tabular.Json.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}:_B92FE44943D4DEC27E1C3FA927231844" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51" + "ScatterAssemblies" + { + "_B92FE44943D4DEC27E1C3FA927231844" + { + "Name" = "8:netstandard.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:netstandard.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}:_C0C5DE7473DD8B2395E58F2E339A1AF1" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices.AdomdClient, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_C0C5DE7473DD8B2395E58F2E339A1AF1" + { + "Name" = "8:Microsoft.AnalysisServices.AdomdClient.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:Microsoft.AnalysisServices.AdomdClient.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}:_C48ECEE77D85A17550522737B8522D2B" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:Microsoft.AnalysisServices, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_C48ECEE77D85A17550522737B8522D2B" + { + "Name" = "8:Microsoft.AnalysisServices.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:Microsoft.AnalysisServices.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}:_C4ACAD76FA650ADD691588B6D2AA3BB2" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:TRUE" + "AssemblyAsmDisplayName" = "8:System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_C4ACAD76FA650ADD691588B6D2AA3BB2" + { + "Name" = "8:System.IO.Compression.FileSystem.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.IO.Compression.FileSystem.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}:_C67DA394422F5927664B4F5D40B66A64" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_C67DA394422F5927664B4F5D40B66A64" + { + "Name" = "8:System.Numerics.Vectors.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.Numerics.Vectors.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}:_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" + "AssemblyIsInGAC" = "11:TRUE" + "AssemblyAsmDisplayName" = "8:System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_DE34EA7F69E9468ED2359C26A753AE38" + { + "Name" = "8:System.IO.Compression.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.IO.Compression.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}:_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" + { + } + "Folder" + { + "{1525181F-901A-416C-8A58-119130FE478E}:_50D538C6BF8441F883642D01FCD5AEB3" + { + "Name" = "8:#1919" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:ProgramMenuFolder" + "Folders" + { + } + } + "{1525181F-901A-416C-8A58-119130FE478E}:_6EFB76AEFB2B461AB66FCC0E49CD2207" + { + "Name" = "8:#1916" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:DesktopFolder" + "Folders" + { + } + } + "{3C67513D-01DD-4637-8A68-80971EB9504F}:_E31DB11D2BA64930BBAB95E69D1A099C" + { + "DefaultLocation" = "8:[ProgramFilesFolder][Manufacturer]\\[ProductName]" + "Name" = "8:#1925" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:TARGETDIR" + "Folders" + { + "{9EF0B969-E518-4E46-987F-47570745A589}:_B58920A79D7F4ECFB278D11291BCEBBB" + { + "Name" = "8:Resources" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:_FAA3AC1E691E4032B2BE7B01A7BAE0B2" + "Folders" + { + } + } + } + } + } + "LaunchCondition" + { + } + "Locator" + { + } + "MsiBootstrapper" + { + "LangId" = "3:1033" + "RequiresElevation" = "11:FALSE" + } + "Product" + { + "Name" = "8:Microsoft Visual Studio" + "ProductName" = "8:Metadata Translator" + "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.3.0" + "Manufacturer" = "8:Analysis Services Samples" + "ARPHELPTELEPHONE" = "8:" + "ARPHELPLINK" = "8:" + "Title" = "8:Metadata Translator Setup" + "Subject" = "8:" + "ARPCONTACT" = "8:Kay Unkroth" + "Keywords" = "8:Metadata, Translation, Power BI, Azure Cognitive Services" + "ARPCOMMENTS" = "8:Metadata Translator can translate the names and descriptions of the metadata objects in your semantic model by using Azure Cognitive Services." + "ARPURLINFOABOUT" = "8:https://github.com/Microsoft/analysis-services" + "ARPPRODUCTICON" = "8:_A00C4C4DA66740A385AE811FF37BAA77" + "ARPIconIndex" = "3:0" + "SearchPath" = "8:" + "UseSystemSearchPath" = "11:TRUE" + "TargetPlatform" = "3:0" + "PreBuildEvent" = "8:" + "PostBuildEvent" = "8:" + "RunPostBuildEvent" = "3:0" + } + "Registry" + { + "HKLM" + { + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_D7D41309A07C4A668200D9B863ADEE00" + { + "Name" = "8:Software" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_03975696FE60414C86BE7A66C5CBC1B4" + { + "Name" = "8:[Manufacturer]" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + } + "Values" + { + } + } + } + "Values" + { + } + } + } + } + "HKCU" + { + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_7C0704A49C564C5FBEB0AD7C80732F51" + { + "Name" = "8:Software" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_96F2E6D56F5F4AEE8304513B8385E6AC" + { + "Name" = "8:[Manufacturer]" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + } + "Values" + { + } + } + } + "Values" + { + } + } + } + } + "HKCR" + { + "Keys" + { + } + } + "HKU" + { + "Keys" + { + } + } + "HKPU" + { + "Keys" + { + } + } + } + "Sequences" + { + } + "Shortcut" + { + } + "UserInterface" + { + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_09003F210C804B6FA4C5A9B160660210" + { + "Name" = "8:#1901" + "Sequence" = "3:2" + "Attributes" = "3:2" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_16811BD769ED491DAF196869AFD7E027" + { + "Sequence" = "3:100" + "DisplayName" = "8:Progress" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminProgressDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "ShowProgress" + { + "Name" = "8:ShowProgress" + "DisplayName" = "8:#1009" + "Description" = "8:#1109" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_1088105BCC5945EEB37AFF0AFF0C4CC6" + { + "UseDynamicProperties" = "11:FALSE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdUserInterface.wim" + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_15845F5C68984FB8A71A8575B2ECF1C1" + { + "Name" = "8:#1901" + "Sequence" = "3:1" + "Attributes" = "3:2" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_9F10CB16426C40D6A8B36E740EAE7BFD" + { + "Sequence" = "3:100" + "DisplayName" = "8:Progress" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdProgressDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "ShowProgress" + { + "Name" = "8:ShowProgress" + "DisplayName" = "8:#1009" + "Description" = "8:#1109" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_41B8F885DFCE440C92FA5B391FB630F6" + { + "Name" = "8:#1900" + "Sequence" = "3:2" + "Attributes" = "3:1" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_0D391AF53FAF4A6CA251244400A2F33C" + { + "Sequence" = "3:300" + "DisplayName" = "8:Confirm Installation" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminConfirmDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_1EDB01EE569149C2ABFFE4DA2757FB5C" + { + "Sequence" = "3:200" + "DisplayName" = "8:Installation Folder" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminFolderDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_85EAE7018C054FABB4596D7E309A5262" + { + "Sequence" = "3:100" + "DisplayName" = "8:Welcome" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminWelcomeDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "CopyrightWarning" + { + "Name" = "8:CopyrightWarning" + "DisplayName" = "8:#1002" + "Description" = "8:#1102" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1202" + "DefaultValue" = "8:#1202" + "UsePlugInResources" = "11:TRUE" + } + "Welcome" + { + "Name" = "8:Welcome" + "DisplayName" = "8:#1003" + "Description" = "8:#1103" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1203" + "DefaultValue" = "8:#1203" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_538BB9E3DFB741609085384AE5795C3C" + { + "Name" = "8:#1902" + "Sequence" = "3:1" + "Attributes" = "3:3" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_CA924473681444A89A49D51D2E53908B" + { + "Sequence" = "3:100" + "DisplayName" = "8:Finished" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdFinishedDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "UpdateText" + { + "Name" = "8:UpdateText" + "DisplayName" = "8:#1058" + "Description" = "8:#1158" + "Type" = "3:15" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1258" + "DefaultValue" = "8:#1258" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_777D8B788F2942718B065E5C46C10CD7" + { + "Name" = "8:#1900" + "Sequence" = "3:1" + "Attributes" = "3:1" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_357D60EBE924490BAA6BE1F1D251E51C" + { + "Sequence" = "3:200" + "DisplayName" = "8:Installation Folder" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdFolderDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "InstallAllUsersVisible" + { + "Name" = "8:InstallAllUsersVisible" + "DisplayName" = "8:#1059" + "Description" = "8:#1159" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_56063236D91C4FF1895700BD03750C4F" + { + "Sequence" = "3:300" + "DisplayName" = "8:Confirm Installation" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdConfirmDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_B71FBCECADEC4E73ADE13F0DCF6D5EB3" + { + "Sequence" = "3:100" + "DisplayName" = "8:Welcome" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdWelcomeDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "CopyrightWarning" + { + "Name" = "8:CopyrightWarning" + "DisplayName" = "8:#1002" + "Description" = "8:#1102" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1202" + "DefaultValue" = "8:#1202" + "UsePlugInResources" = "11:TRUE" + } + "Welcome" + { + "Name" = "8:Welcome" + "DisplayName" = "8:#1003" + "Description" = "8:#1103" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1203" + "DefaultValue" = "8:#1203" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_DB9992FAA9334AEAA7EC2D20A38C045B" + { + "Name" = "8:#1902" + "Sequence" = "3:2" + "Attributes" = "3:3" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_0622BCDDFC264266861F06498C4DA298" + { + "Sequence" = "3:100" + "DisplayName" = "8:Finished" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminFinishedDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_FB8DB1E4746043989F7A2717AC661CDF" + { + "UseDynamicProperties" = "11:FALSE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdBasicDialogs.wim" + } + } + "MergeModule" + { + } + "ProjectOutput" + { + "{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_2CE91AB829584CF99B8C35A6E138B5FF" + { + "SourcePath" = "8:..\\MTCmd\\obj\\Debug\\MTCmd.exe" + "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:FALSE" + "IsolateTo" = "8:" + "ProjectOutputGroupRegister" = "3:1" + "OutputConfiguration" = "8:" + "OutputGroupCanonicalName" = "8:Built" + "OutputProjectGuid" = "8:{0B0EA3E1-79CC-4674-A763-9C40925DEE31}" + "ShowKeyOutput" = "11:TRUE" + "ExcludeFilters" + { + } + } + "{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_5EE4AF833E364CBAB977AFCB8CD5E99D" + { + "SourcePath" = "8:..\\Metadata Translator\\obj\\Debug\\Metadata Translator.exe" + "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:FALSE" + "IsolateTo" = "8:" + "ProjectOutputGroupRegister" = "3:1" + "OutputConfiguration" = "8:" + "OutputGroupCanonicalName" = "8:Built" + "OutputProjectGuid" = "8:{CB7D493C-B67E-4438-B304-EFE5D418ADDF}" + "ShowKeyOutput" = "11:TRUE" + "ExcludeFilters" + { + } + } + } + } +} diff --git a/MetadataTranslator/Metadata Translator.sln b/MetadataTranslator/Metadata Translator.sln new file mode 100644 index 0000000..cbd52ea --- /dev/null +++ b/MetadataTranslator/Metadata Translator.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30907.101 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Metadata Translator", "Metadata Translator\Metadata Translator.csproj", "{CB7D493C-B67E-4438-B304-EFE5D418ADDF}" +EndProject +Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Metadata Translator Setup", "Metadata Translator Setup\Metadata Translator Setup.vdproj", "{FC353DA7-B669-4EDF-B465-CB9827C7F035}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{13A368ED-B955-438D-9365-03E467A59DB0}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MTCmd", "MTCmd\MTCmd.csproj", "{0B0EA3E1-79CC-4674-A763-9C40925DEE31}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CB7D493C-B67E-4438-B304-EFE5D418ADDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB7D493C-B67E-4438-B304-EFE5D418ADDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB7D493C-B67E-4438-B304-EFE5D418ADDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB7D493C-B67E-4438-B304-EFE5D418ADDF}.Release|Any CPU.Build.0 = Release|Any CPU + {FC353DA7-B669-4EDF-B465-CB9827C7F035}.Debug|Any CPU.ActiveCfg = Debug + {FC353DA7-B669-4EDF-B465-CB9827C7F035}.Release|Any CPU.ActiveCfg = Release + {0B0EA3E1-79CC-4674-A763-9C40925DEE31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B0EA3E1-79CC-4674-A763-9C40925DEE31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B0EA3E1-79CC-4674-A763-9C40925DEE31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B0EA3E1-79CC-4674-A763-9C40925DEE31}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0733CEFA-6458-4D8F-B498-FC9827973880} + EndGlobalSection +EndGlobal diff --git a/MetadataTranslator/Metadata Translator/App.config b/MetadataTranslator/Metadata Translator/App.config new file mode 100644 index 0000000..511e16c --- /dev/null +++ b/MetadataTranslator/Metadata Translator/App.config @@ -0,0 +1,30 @@ + + + + +
+ + + + + + + + + + + + https://api.cognitive.microsofttranslator.com/ + + + + + + False + + + + + + + \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/App.xaml b/MetadataTranslator/Metadata Translator/App.xaml new file mode 100644 index 0000000..bd6ce63 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/MetadataTranslator/Metadata Translator/App.xaml.cs b/MetadataTranslator/Metadata Translator/App.xaml.cs new file mode 100644 index 0000000..268b902 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/App.xaml.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace Metadata_Translator +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + private void Application_Startup(object sender, StartupEventArgs e) + { + MainWindow mainWindow = new MainWindow(e); + mainWindow.Show(); + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Converters/CollectionEmptyTrueFalseConverter.cs b/MetadataTranslator/Metadata Translator/Converters/CollectionEmptyTrueFalseConverter.cs new file mode 100644 index 0000000..343e0ec --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Converters/CollectionEmptyTrueFalseConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Dynamic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace Metadata_Translator +{ + public class CollectionEmptyTrueFalseConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (value is ObservableCollection collection) ? collection.Count > 0 : false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + +} diff --git a/MetadataTranslator/Metadata Translator/Converters/PercentageConverter.cs b/MetadataTranslator/Metadata Translator/Converters/PercentageConverter.cs new file mode 100644 index 0000000..ea8804f --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Converters/PercentageConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace Metadata_Translator +{ + public class PercentageConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Double size) + { + return size * 0.5; + } + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Converters/TranslationPropertyToolTipConverter.cs b/MetadataTranslator/Metadata Translator/Converters/TranslationPropertyToolTipConverter.cs new file mode 100644 index 0000000..d324b5c --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Converters/TranslationPropertyToolTipConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Dynamic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace Metadata_Translator +{ + public class TranslationPropertyToolTipConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if(parameter is string toolTipString) + { + string[] toolTip = toolTipString.Split('|'); + if (value is ObservableCollection collection && toolTip?.Length == 2) + { + return collection.Count > 0 ? toolTip[0] : toolTip[1]; + } + else + return toolTipString; + } + + return string.Empty; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Converters/TrueFalseVisibilityConverter.cs b/MetadataTranslator/Metadata Translator/Converters/TrueFalseVisibilityConverter.cs new file mode 100644 index 0000000..50423ea --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Converters/TrueFalseVisibilityConverter.cs @@ -0,0 +1,48 @@ +using Microsoft.AnalysisServices; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace Metadata_Translator +{ + class TrueFalseVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if(value is bool notVisible) + { + return notVisible ? Visibility.Collapsed : Visibility.Visible; + } + else + return (value == null)? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + class FalseTrueVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool notVisible) + { + return notVisible ? Visibility.Visible : Visibility.Collapsed; + } + else + return (value == null) ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Data/CsvRow.cs b/MetadataTranslator/Metadata Translator/Data/CsvRow.cs new file mode 100644 index 0000000..1559136 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Data/CsvRow.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public class CsvRow + { + public string Type { get; set; } + public string Original { get; set; } + public string Translation { get; set; } + } +} diff --git a/MetadataTranslator/Metadata Translator/Data/DataModel.cs b/MetadataTranslator/Metadata Translator/Data/DataModel.cs new file mode 100644 index 0000000..7cb7fb0 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Data/DataModel.cs @@ -0,0 +1,630 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AS = Microsoft.AnalysisServices; +using Microsoft.AnalysisServices.Tabular; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Dynamic; +using System.Windows.Controls; +using System.Windows.Data; +using System.IO; +using System.Web.Script.Serialization; +using Microsoft.VisualBasic.FileIO; +using Adomd = Microsoft.AnalysisServices.AdomdClient; +using System.Text.RegularExpressions; +using System.Security.Cryptography; +using System.Resources; +using System.Collections; + +namespace Metadata_Translator +{ + public class DataModel + { + Model Model { get; set; } + + public string ContainerColumnHeader { get => "Object"; } + public ObservableCollection Captions { get; private set; } + public ObservableCollection Descriptions { get; private set; } + public ObservableCollection DisplayFolders { get; private set; } + + public string DefaultCulture { get; set; } + + public string ServerName { get; private set; } + public string DatabaseName { get; private set; } + + public List CultureNames + { + get + { + List cultures = new List { Model.Culture }; + cultures.AddRange(Model?.Cultures.Where(i => !i.Name.Equals(Model.Culture)).Select(x => x.Name).ToList()); + return cultures; + } + } + public List SupportedLanguages { get; private set; } + public List SelectedLanguages { get => SupportedLanguages?.Where(x => x.IsSelected==true).ToList(); } + public bool HasTargetLanguages { get => SelectedLanguages?.Count > 1; } + + private const string appTag = "__MT"; + + /// + /// Connect to the dataset by using server and database name. This is how external tools typically connect to a dataset inside of Power BI Desktop. + /// + /// + /// + public DataModel(string server, string database) + { + ServerName = server; + DatabaseName = database; + + Server pbiDesktop = new Server(); + pbiDesktop.Connect($"Data Source={ServerName}"); + Database dataset = pbiDesktop.Databases.GetByName(DatabaseName); + Model = dataset.Model; + + Initialize(); + } + + /// + /// Connects to a dataset using a connection string. This is how tools typically connect to online datasets in SQL Server Analysis Services, Azure Analysis Services, and Power BI. + /// + /// + public DataModel(string connectionString) + { + /// Connect using the full connection string, as it may contain more than + /// just data source and intial catalog, such as user id and password. + /// + Server pbiDesktop = new Server(); + pbiDesktop.Connect(connectionString); + + /// Parse the connection string using regex to avoid resolving server and database names through the AMO objects. + /// + RegexOptions options = RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.Compiled; + foreach (Match match in Regex.Matches(connectionString, "([^=;]*)=([^=;]*)", options)) + { + string[] parts = match.Value.Split('='); + if (parts?.Length == 2 && parts[0].ToLower() == "data source") + { + ServerName = parts[1]; + } + else if (parts?.Length == 2 && parts[0].ToLower() == "initial catalog") + { + DatabaseName = parts[1]; + } + } + + /// Select the database based on the extracted database name. + /// + Database dataset = pbiDesktop.Databases.GetByName(DatabaseName); + Model = dataset.Model; + + Initialize(); + } + + /// + /// Initializes the list of supported languages and the named object collections. + /// + private void Initialize() + { + LoadLanguages(); + DefaultCulture = Model.Culture; + LoadNamedObjectCollections(); + } + + /// + /// A static helper to get the DataModel object based on server and database name. + /// + /// + /// + /// + public static DataModel Connect(string server, string database) + { + return new DataModel(server, database); + } + + /// + /// A static helper to get the DataModel object based on a connection string. + /// + /// + /// + public static DataModel Connect(string connectionString) + { + return new DataModel(connectionString); + } + + /// + /// Gets the tables from the dataset and within it all the columns, measures, and hierarchies + /// and adds these tabular objects to the collections for captions, descriptions, and display folders. + /// + private void LoadNamedObjectCollections() + { + Captions = new ObservableCollection(); + Descriptions = new ObservableCollection(); + DisplayFolders = new ObservableCollection(); + + CultureCollection cultures = Model.Cultures; + + Captions.Add(CreateRow(new MetadataObjectContainer(Model, TranslatedProperty.Caption), Model.Name, DefaultCulture, cultures)); + if (!string.IsNullOrEmpty(Model.Description)) + Descriptions.Add(CreateRow(new MetadataObjectContainer(Model, TranslatedProperty.Description), Model.Description, DefaultCulture, cultures)); + + foreach (Table table in Model.Tables) + { + Captions.Add(CreateRow(new MetadataObjectContainer(table, TranslatedProperty.Caption), table.Name, DefaultCulture, cultures)); + if (!string.IsNullOrEmpty(table.Description)) + Descriptions.Add(CreateRow(new MetadataObjectContainer(table, TranslatedProperty.Description), table.Description, DefaultCulture, cultures)); + foreach (Column column in table.Columns) + { + if (column.Type != ColumnType.RowNumber) + { + Captions.Add(CreateRow(new MetadataObjectContainer(column, TranslatedProperty.Caption), column.Name, DefaultCulture, cultures)); + + if (!string.IsNullOrEmpty(column.Description)) + Descriptions.Add(CreateRow(new MetadataObjectContainer(column, TranslatedProperty.Description), column.Description, DefaultCulture, cultures)); + if (!string.IsNullOrEmpty(column.DisplayFolder)) + DisplayFolders.AddDisplayFolder(column, column.DisplayFolder, DefaultCulture, cultures); + } + } + + foreach (Measure measure in table.Measures) + { + Captions.Add(CreateRow(new MetadataObjectContainer(measure, TranslatedProperty.Caption), measure.Name, DefaultCulture, cultures)); + + if (!string.IsNullOrEmpty(measure.Description)) + Descriptions.Add(CreateRow(new MetadataObjectContainer(measure, TranslatedProperty.Description), measure.Description, DefaultCulture, cultures)); + if (!string.IsNullOrEmpty(measure.DisplayFolder)) + DisplayFolders.AddDisplayFolder(measure, measure.DisplayFolder, DefaultCulture, cultures); + } + + foreach (Hierarchy hierarchy in table.Hierarchies) + { + Captions.Add(CreateRow(new MetadataObjectContainer(hierarchy, TranslatedProperty.Caption), hierarchy.Name, DefaultCulture, cultures)); + + if (!string.IsNullOrEmpty(hierarchy.Description)) + Descriptions.Add(CreateRow(new MetadataObjectContainer(hierarchy, TranslatedProperty.Description), hierarchy.Description, DefaultCulture, cultures)); + if (!string.IsNullOrEmpty(hierarchy.DisplayFolder)) + DisplayFolders.AddDisplayFolder(hierarchy, hierarchy.DisplayFolder, DefaultCulture, cultures); + } + } + } + + /// + /// Loads the list of supported languages from the supportedlanguages.json file. + /// + private void LoadLanguages() + { + SupportedLanguages = new List(); + string content = File.ReadAllText($"{System.AppDomain.CurrentDomain.BaseDirectory}Resources\\supportedlanguages.json"); + foreach (Language lang in new JavaScriptSerializer().Deserialize>(content)) + { + SupportedLanguages.Add(lang); + } + } + + /// + /// Marks the languages in the list of supported lanuages as selected and model default. + /// + public void InitializeLanguages() + { + if (this.CultureNames?.Count > 0) + { + /// First the model culture. + /// + SetLanguageFlags(this.CultureNames[0], true, true); + + /// Then all others + /// + for (int i = 1; i < this.CultureNames.Count; i++) + { + SetLanguageFlags(this.CultureNames[i], true, false); + } + } + } + + /// + /// Marks all translation languages as unselected. + /// + public void DeselectAllLanguages() + { + foreach(Language lang in SelectedLanguages) + { + if (lang.IsNotModelDefault) + lang.IsSelected = false; + } + } + + /// + /// Marks a language specified by lcid as selected and as model default. + /// + /// + /// + /// + public bool SetLanguageFlags(string lcid, bool isSelected, bool isModelDefault = false) + { + if (lcid.Equals(DefaultCulture, StringComparison.InvariantCultureIgnoreCase) && isModelDefault == false) + { + return false; + } + + Language language = this.SupportedLanguages.Where(x => x.LanguageTag.Equals(lcid)).FirstOrDefault(); + if (language != null) + { + language.IsSelected = isSelected; + language.IsModelDefault = isModelDefault; + return true; + } + else + { + return false; + } + } + + /// + /// Gets the Language object based on the lcid (i.e. LanguageTag). + /// + /// + /// + public Language GetLanguageByLcid(string lcid) + { + return this.SupportedLanguages.Where(x => x.LanguageTag.Equals(lcid)).FirstOrDefault(); + } + + /// + /// Creates a new ExpandoObject for a source string (displayString). + /// + /// + /// + /// + /// + /// An ExpandoObject representing a data row. + public ExpandoObject CreateRow(MetadataObjectContainer objectContainer, string displayString, string defaultCulture, CultureCollection cultures) + { + dynamic row = new ExpandoObject(); + + ((IDictionary)row)[ContainerColumnHeader] = objectContainer; + foreach (var culture in cultures) + { + ((IDictionary)row)[culture.Name] = culture.Name.Equals(defaultCulture) ? displayString : + culture.ObjectTranslations[objectContainer.TabularObject, objectContainer.TranslatedProperty]?.Value; + } + + return row; + } + + /// + /// Combine all collections for translation and updating. + /// + /// + public List GetAllDataRows() + { + var allRows = new List(); + foreach (var item in Captions) allRows.Add(item); + foreach (var item in Descriptions) allRows.Add(item); + foreach (var item in DisplayFolders) allRows.Add(item); + return allRows; + } + + /// + /// Adds a translation to a Tabular metadata object. + /// + /// + /// + /// + private void SetTranslation(Culture culture, MetadataObjectContainer metadataObjectContainer, string translation) + { + culture.ObjectTranslations.SetTranslation( + metadataObjectContainer.TabularObject, metadataObjectContainer.TranslatedProperty, + translation); + } + + /// + /// Updates the Power BI dataset with the translations from the ExpandoObject collections and saves the changes. + /// + public void Update() + { + /// Delete any deselected cultures that still exist in the dataset. + /// + List cultureNames = SelectedLanguages?.Select(sl => sl.LanguageTag)?.ToList(); + + /// There must be at least the default culture in the cultureNames. + /// + if (cultureNames == null || cultureNames.Count < 1) return; + + var culturesToRemove = CultureNames.Where(cn1 => !cultureNames.Any(cn2 => cn2.Equals(cn1))).ToList(); + culturesToRemove.Remove(DefaultCulture); + + foreach(string cultureName in culturesToRemove) + { + if (Model.Cultures.Contains(cultureName)) + { + Model.Cultures.Remove(cultureName); + } + } + + /// Add any newly selected cultures. + /// + foreach (string cultureName in cultureNames) + { + if (!Model.Cultures.Contains(cultureName)) + { + Model.Cultures.Add(new Culture { Name = cultureName }); + } + } + + /// Add the translations to all the metadata objects. + /// + foreach (ExpandoObject row in GetAllDataRows()) + { + if (((IDictionary)row)[ContainerColumnHeader] is MetadataObjectContainer metadataObjectContainer) + { + /* + * Include this part when updating the default culture (i.e. updating the actual metadata objects) is supported. + * + switch (metadataObjectContainer.TranslatedProperty) + { + case TranslatedProperty.Caption: + metadataObjectContainer.TabularObject.Name = row.GetValue(DefaultCulture); + break; + case TranslatedProperty.Description: + if (metadataObjectContainer.TabularObject is Table table) + { + table.Description = row.GetValue(DefaultCulture); + } + else if (metadataObjectContainer.TabularObject is Column col) + { + col.Description = row.GetValue(DefaultCulture); + } + else if (metadataObjectContainer.TabularObject is Measure measure) + { + measure.Description = row.GetValue(DefaultCulture); + } + else if (metadataObjectContainer.TabularObject is Hierarchy hierarchy) + { + hierarchy.Description = row.GetValue(DefaultCulture); + } + break; + case TranslatedProperty.DisplayFolder: + if (metadataObjectContainer.TabularObject is Column column) + { + column.DisplayFolder = row.GetValue(DefaultCulture); + } + else if (metadataObjectContainer.TabularObject is Measure measure) + { + measure.DisplayFolder = row.GetValue(DefaultCulture); + } + else if (metadataObjectContainer.TabularObject is Hierarchy hierarchy) + { + hierarchy.DisplayFolder = row.GetValue(DefaultCulture); + } + break; + } + */ + foreach (string cultureName in cultureNames) + { + SetTranslation(Model.Cultures[cultureName], + metadataObjectContainer, + row.GetValue(cultureName)); + } + } + } + + /// Save the changes in the dataset. + /// + Annotation mtAnnotation = new Annotation(); + mtAnnotation.Name = appTag; + mtAnnotation.Value = "1"; + if (!Model.Annotations.Contains(appTag)) + { + Model.Annotations.Add(mtAnnotation); + } + + Model.Database.Update(AS.UpdateOptions.ExpandFull); + } + + /// + /// Exports the translations to individual language (csv) files. + /// The files are placed into the specified export folder. + /// + /// + public void ExportToCsv(string exportFolderPath) + { + string separator = ","; + List dataRows = GetAllDataRows(); + if (dataRows != null && dataRows.Count > 0) + { + List languages = SelectedLanguages.Where(l => l.IsModelDefault != true).Select(l => l.LanguageTag).ToList(); + + if (languages != null && languages.Count > 0) + { + foreach (string lcid in languages) + { + StringBuilder csvContent = new StringBuilder(); + csvContent.AppendLine("Type,Original,Translation"); + + foreach (var stringValues in dataRows.GetValues(ContainerColumnHeader, DefaultCulture, lcid)) + { + csvContent.AppendLine( + string.Join( + separator, + new string[] { + stringValues.Type.ToCsvString(), + stringValues.Original.ToCsvString(), + stringValues.Translation.ToCsvString() + }) + ); + } + + using (var sw = File.Create(System.IO.Path.Combine(exportFolderPath, $"{lcid}.csv"))) + { + var preamble = Encoding.UTF8.GetPreamble(); + sw.Write(preamble, 0, preamble.Length); + var data = Encoding.UTF8.GetBytes(csvContent.ToString()); + sw.Write(data, 0, data.Length); + } + } + } + } + } + + /// + /// Exports the translations to individual resx files. + /// The files are placed into the specified export folder. + /// + /// + public void ExportToResx(string exportFolderPath, string keyPrefix) + { + List dataRows = GetAllDataRows(); + if (dataRows != null && dataRows.Count > 0) + { + List languages = SelectedLanguages.Select(l => l.LanguageTag).ToList(); + + if (languages != null && languages.Count > 0) + { + foreach (string lcid in languages) + { + using (ResXResourceWriter resx = new ResXResourceWriter(System.IO.Path.Combine(exportFolderPath, $"{lcid}.resx"))) + { + foreach(var kvp in dataRows.GetKeyValuePairs(ContainerColumnHeader, lcid, keyPrefix)) + { + resx.AddResource(kvp.Key.ToString(), kvp.Value); + } + } + } + } + } + } + + /// + /// Imports translations from a csv file. The file name must match the LCID of the target language. + /// + /// + /// + /// + public void ImportFromCsv(string filePath, string lcid, bool replaceExistingTranslations, bool fallbackToDefaultLocale = false) + { + string csvData = File.ReadAllText(filePath); + if (string.IsNullOrEmpty(csvData)) return; + + List parsedRows = new List(); + + using (TextFieldParser parser = new TextFieldParser(new StringReader(csvData))) + { + 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) + { + parsedRows.Add(new CsvRow + { + Type = textFields[0], + Original = textFields[1], + Translation = (fallbackToDefaultLocale && string.IsNullOrEmpty(textFields[2]))? textFields[1] : textFields[2] + }); + } + } + } + + ApplyTranslation(lcid, parsedRows, replaceExistingTranslations); + } + + public void ImportFromResx(string filePath, string referencePath, string lcid, bool replaceExistingTranslations, bool fallbackToDefaultLocale) + { + 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 (fallbackToDefaultLocale && string.IsNullOrEmpty(translation)) + translation = value; + + 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); + } + + /// + /// Applies a list of translations to the ExpandoObject collections + /// + /// + /// + /// + 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)) + { + /// Not all rows matched, so let's do this the slow way + /// matching strings. + /// + foreach(ExpandoObject row in allDataRows) + { + var metaContainer = (MetadataObjectContainer)row.GetObject(ContainerColumnHeader); + var original = row.GetValue(DefaultCulture); + 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); + } + } + } + } + + /// + /// Iterates over the dataRows and applies the translated strings with the assumption that + /// translatedRows matches the dataRows in number and order. + /// + private bool MatchAllRows(List dataRows, string lcid, List translatedRows, bool replaceExistingTranslations) + { + if(dataRows == null || dataRows.Count != translatedRows?.Count) + return false; + + for(int i = 0; i < translatedRows.Count; i++) + { + ExpandoObject row = dataRows[i]; + CsvRow csvRow = translatedRows[i]; + + if (row.GetValue(DefaultCulture) != csvRow.Original) + return false; + + row.SetValue(lcid, csvRow.Translation, replaceExistingTranslations); + } + + return true; + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Data/DisplayFolderContainer.cs b/MetadataTranslator/Metadata Translator/Data/DisplayFolderContainer.cs new file mode 100644 index 0000000..cf4d18f --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Data/DisplayFolderContainer.cs @@ -0,0 +1,46 @@ +using Microsoft.AnalysisServices.Tabular; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public class DisplayFolderContainer : MetadataObjectContainer + { + public override NamedMetadataObject TabularObject { get => TabularObjects.FirstOrDefault(); protected set { } } + public List TabularObjects { get; private set; } + + public DisplayFolderContainer(NamedMetadataObject metadataObject, TranslatedProperty translatedProperty) : base(metadataObject, translatedProperty) + { + TabularObjects = new List(); + TabularObjects.Add(metadataObject); + } + + public override string ToString() + { + return (TabularObjects.Count > 1)? + $"DisplayFolder - {TabularObjects.Count} Objects" : + $"DisplayFolder - 1 {TabularObject.ObjectType}"; + } + + protected override string GetFullName(MetadataObject metadataObject) + { + if (metadataObject is Measure measure) + { + return "#" + Convert.ToBase64String(Encoding.UTF8.GetBytes(measure.DisplayFolder)); + } + else if (metadataObject is Column column) + { + return "#" + Convert.ToBase64String(Encoding.UTF8.GetBytes(column.DisplayFolder)); + } + else if (metadataObject is Hierarchy hierarchy) + { + return "#" + Convert.ToBase64String(Encoding.UTF8.GetBytes(hierarchy.DisplayFolder)); + } + + return string.Empty; + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Data/MetadataObjectContainer.cs b/MetadataTranslator/Metadata Translator/Data/MetadataObjectContainer.cs new file mode 100644 index 0000000..e9661c2 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Data/MetadataObjectContainer.cs @@ -0,0 +1,56 @@ +using Microsoft.AnalysisServices.Tabular; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public class MetadataObjectContainer + { + public virtual NamedMetadataObject TabularObject { get; protected set; } + public TranslatedProperty TranslatedProperty { get; protected set; } + + public Guid TemporaryObjectId { get; protected set; } + public MetadataObjectContainer(NamedMetadataObject metadataObject, TranslatedProperty translatedProperty) + { + TabularObject = metadataObject; + TranslatedProperty = translatedProperty; + TemporaryObjectId = Guid.NewGuid(); + } + + public override string ToString() + { + switch(TranslatedProperty) + { + case TranslatedProperty.Caption: + return $"{TabularObject.ObjectType} - Caption"; + case TranslatedProperty.Description: + return $"{TabularObject.ObjectType} - Description"; + case TranslatedProperty.DisplayFolder: + return $"{TabularObject.ObjectType} - DisplayFolder"; + default: + return TabularObject.ObjectType.ToString(); + } + } + + public virtual string GetUniqueName(string namePrefix) + { + return (string.IsNullOrEmpty(namePrefix)) ? TemporaryObjectId.ToString() : $"{namePrefix}{GetFullName(TabularObject)}#{TranslatedProperty}"; + } + + protected virtual string GetFullName(MetadataObject metadataObject) + { + string fullName = string.Empty; + + if(metadataObject is NamedMetadataObject tabularObject) + { + string parentName = (tabularObject.Parent != null) ? GetFullName(tabularObject.Parent) : string.Empty; + fullName = string.Concat(parentName, "#", tabularObject.Name); + } + + return fullName; + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/A resx with static key names instead of random GUIDs.png b/MetadataTranslator/Metadata Translator/Documentation/Images/A resx with static key names instead of random GUIDs.png new file mode 100644 index 0000000..c8d0362 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/A resx with static key names instead of random GUIDs.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/Adding languages.png b/MetadataTranslator/Metadata Translator/Documentation/Images/Adding languages.png new file mode 100644 index 0000000..5f98762 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/Adding languages.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/Applying translations.png b/MetadataTranslator/Metadata Translator/Documentation/Images/Applying translations.png new file mode 100644 index 0000000..3c53ca2 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/Applying translations.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/ConnectionStringToAPowerBIDataset.png b/MetadataTranslator/Metadata Translator/Documentation/Images/ConnectionStringToAPowerBIDataset.png new file mode 100644 index 0000000..3fa4b2f Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/ConnectionStringToAPowerBIDataset.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/Export.png b/MetadataTranslator/Metadata Translator/Documentation/Images/Export.png new file mode 100644 index 0000000..cf84356 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/Export.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/Import.png b/MetadataTranslator/Metadata Translator/Documentation/Images/Import.png new file mode 100644 index 0000000..5203024 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/Import.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/ImportExport.png b/MetadataTranslator/Metadata Translator/Documentation/Images/ImportExport.png new file mode 100644 index 0000000..fed32e4 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/ImportExport.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/Installing Metadata Translator.png b/MetadataTranslator/Metadata Translator/Documentation/Images/Installing Metadata Translator.png new file mode 100644 index 0000000..ee737e8 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/Installing Metadata Translator.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/Launching Metadata Translator.png b/MetadataTranslator/Metadata Translator/Documentation/Images/Launching Metadata Translator.png new file mode 100644 index 0000000..8a6988d Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/Launching Metadata Translator.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/MT command-line app.png b/MetadataTranslator/Metadata Translator/Documentation/Images/MT command-line app.png new file mode 100644 index 0000000..6160b19 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/MT command-line app.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/MetadataTranslator.png b/MetadataTranslator/Metadata Translator/Documentation/Images/MetadataTranslator.png new file mode 100644 index 0000000..49eaf96 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/MetadataTranslator.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/Settings.png b/MetadataTranslator/Metadata Translator/Documentation/Images/Settings.png new file mode 100644 index 0000000..120080f Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/Settings.png differ diff --git a/MetadataTranslator/Metadata Translator/Documentation/Images/Translate.png b/MetadataTranslator/Metadata Translator/Documentation/Images/Translate.png new file mode 100644 index 0000000..630a203 Binary files /dev/null and b/MetadataTranslator/Metadata Translator/Documentation/Images/Translate.png differ 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 new file mode 100644 index 0000000..2d6f459 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Documentation/README.md @@ -0,0 +1,257 @@ +# Metadata Translator + +Metadata Translator helps to streamline the localization of Power BI data models. The tool can automatically translate the captions, descriptions, and display folder names of tables, columns, measures, and hierarchies by using the machine translation technology of Azure Cognitive Services. It also lets you export and import translations via Comma Separated Values (.csv) files for convenient bulk editing in Excel or a localization tool. + +![MetadataTranslator](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/MetadataTranslator.png) + +> Note +> +> Metadata Translator can add translations to datasets hosted in SQL Server Analysis Services, Azure Analysis Services, and Power BI. However, Power BI only supports multiple locales for dataset hosted on Power BI Premium, Premium per User, or a Power BI Embedded SKU. Datasets on shared capacity cannot take advantage of metadata translations yet. + +## Installation + +Metadata Translator can be installed by using a Windows Installer Package (.msi) file. The Metadata Translator solution includes a Metadata Translator Setup project to build this .msi file. + +Installing Metadata Translator + +Start the installation by launching the .msi file, accept the default settings or change the installation folder if desired, and then confirm the remaining prompts to complete the installation. To remove Metadata Translator, launch the .msi file again and choose Remove Metadata Translator, or use Add/Remove Programs. + +## Starting Metadata Translator + +Metadata Translator integrates with Power BI Desktop as an external tool. To start it, open a Power BI Desktop (.pbix) file, switch to the External Tools ribbon, and click on Metadata Translator. Metadata Translator automatically connects to the dataset insides the .pbix file and lets you apply changes to this dataset. + +Launching Metadata Translator + +## Configuration settings + +Metadata Translator uses the cloud-based machine translation services of Azure Cognitive Services and requires a subscription key and an endpoint URL to call the Translator REST API. See [Create a Translator resource](https://docs.microsoft.com/azure/cognitive-services/Translator/translator-how-to-signup) in the product documentation. To get started, you'll need an active Azure account. If you don't have one, you can create a free 12-month subscription. For the purposes of Metadata Translator, it is also sufficient to create a free Translator instance in most cases. See [Cognitive Services pricing—Translator](https://azure.microsoft.com/pricing/details/cognitive-services/translator/) for more details. + +To register a Translator instance in Metadata Translator, click on the Settings button in the toolbar and provide the required information, which is generated when you provision the Translator resource in the Azure portal. Enable the Overwrite Translation checkbox if you want to replace all existing translations every time you click on Translate. + +Settings + +> **Note** +> +> Configuring Metadata Translator to use Azure Cognitive Services is optional. The configuration is recommended but not required if you prefer to translate the strings manually. + +## Choosing languages + +Metadata Translator enables you to choose from over 300 cultures in all languages that Power BI supports. Click on Languages to pick the cultures you want to add to your dataset. Although there is theoretically no limit regarding the number of cultures you can add, it is important to keep in mind that every translation increases the size of the dataset. + +![Adding languages](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/Adding%20languages.png) + +For every language selected in the Language pane, Metadata Translator adds a column to the translation grid. Deselecting a language removes the corresponding translation grid column. + +> **Note** +> +> You cannot remove the default language of the dataset, which is the language marked with an asterisk in the translation grid. + +## Performing a machine translation + +Having added a new language, click on the Translate button in the toolbar to fill the corresponding column in the translation grid with the translated strings. If you haven’t registered a translation service endpoint yet, Metadata Translator automatically opens the Settings pane for you to provide the required configuration settings. See Configuration settings earlier in this readme. + +![Translate](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/Translate.png) + +## Reviewing translated strings + +The translation grid organizes the translation based on the translated property. Choose Caption, Description, or Display Folder in the toolbar. If the current dataset does not include any descriptions or display folders, the corresponding radio buttons are greyed out. + +## Editing translated strings + +The translation grid also lets you add translated strings and edit any existing strings manually. Just double-click the desired grid cell and make the changes. + +> **Note** +> +> You cannot edit the strings of the default language. The default language, marked with an asterisk, is read-only in the translation grid. + +## Exporting to .CSV files + +A dataset typically includes hundreds of strings. For convenient bulk editing in Excel or a localization tool, click on Import/Export to open the Import/Export pane, and then click Export. Be aware that Metadata Translators overwrites any existing files in the export folder without warning. It is recommended to create a new folder for each export to avoid accidental overwrites.![Export](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/Export.png) + +> **Note** +> +> Metadata Translators exports each culture into a separate .csv file based on the locale identifier (LCID). + +## Importing from .CSV files + +To import translated strings from a .csv file, make sure the file name(s) correspond(s) to the locale identifier (LCID) of the target language(s). Click on Import/Export to open the Import/Export pane, and then click Import. Choose the files in the Open dialog box and click Open. + +![Import](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/Import.png) + +If you import a .csv file for a culture that you haven’t added to the dataset yet, Metadata Translator automatically adds the culture for you and displays the corresponding column in the translation grid with the imported strings. + +> **Note** +> +> During the import operation, Metadata Translators first expects a full match of the default strings including their ordering. If the ordering is different, Metadata Translators switches to case-sensitive string matching. Any strings that don’t have an exact match are ignored and the corresponding cells remain empty in the translation grid. + +## Applying translations to a dataset + +Metadata Translator detects the default language and all translations in the dataset and reads corresponding the captions, descriptions, and display folder names on startup. As you work with the translation grid, add or remove cultures, perform machine translations, or import translated strings, the changes only affect the data in Metadata Translator. To apply the changes to the dataset, click on the Apply button in the toolbar, and then save the .pbix file in Power BI Desktop to persist the changes. + +![Applying translations](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/Applying%20translations.png) + +## Connecting to an online dataset + +Metadata Translator can also connect to data models hosted in SQL Server Analysis Services, Azure Analysis Services, and the Power BI service so you can add translations to an online dataset. Simply start Metadata Translator directly from the installation folder, type the full dataset connection string in the input form, and then click OK to connect. + +![Connection string to a PowerBI dataset](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/ConnectionStringToAPowerBIDataset.png) + +> Note + +> You must provide the full connection string. This is especially important to keep in mind when connecting to a dataset in the Power BI service. The connection string that Power BI displays on the dataset settings page does not include the Data Source property name. It is an incomplete connection string, such as *powerbi://api.powerbi.com/v1.0/myorg/AdventureWorksSource;initial catalog=AdventureWorks*. Make sure to add "Data Source=" in front of it. The screenshot above shows the full connection string: *data source=powerbi://api.powerbi.com/v1.0/myorg/AdventureWorksSource;initial catalog=AdventureWorks*. + +## Command-line operations + +Metadata Translator supports command-line operations through a thin console application called MTCmd.exe so that you can import and export translations in an automated way. You can find MTCmd.exe in the Metadata Translator installation folder. Run MTCmd /? to display available command-line options and operations, as in the following screenshot. + +![Metadata Translator command-line app](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/MT%20command-line%20app.png) + +### Connecting to a dataset + +In order to connect to a dataset hosted in SQL Server Analysis Services, Azure Analysis Services, or the Power BI service, you must specify the full connection string by using the --connection-string parameter (or -cs). See also the previous "*Connecting to an online dataset*" section. The --connection-string parameter is mandatory for all export and import operations. + +### Exporting translations + +To export existing translations from a dataset, you must specify full path to an export folder by using the --export-folder (-ef) parameter. For example, the following command exports all translations 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` + +> Note +> +> The specified export folder must exist prior to running the command-line app. 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 culture into a separate .csv file based on the locale identifier (LCID). + +### Generating new translations + +The -ef option exports existing translations from a dataset. If you want to add a new translation, you can specify the corresponding locale identifier by using the --locale-id (-lcid) option. For example, the following command generates a translation file in the aforementioned export folder for the locale 'de-DE': + +`MTCmd -cs "powerbi://api.powerbi.com/v1.0/myorg/AdventureWorksSource;initial catalog=AdventureWorks" -ef C:\ExportedTranslations -lcid "de-DE" ` + +> Note +> +> If a translation exists for the specified locale identifier, MTCmd exports the existing translated strings. If a translation does not exist, MTCmd generates a translation file without translated strings. You can then add the translations by using a localization tool and import the file as explained in the next section to add the translations to the dataset. For a list of supported locale identifiers, refer to the [supportedlanguages.json]('https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata Translator/Resources/supportedlanguages.json') file under Resources in the Metadata Translator project folder. + +### Importing translations + +To import translations from a .csv file, you must specify full path to the import file by using the --import-file (-if) parameter. The file name must correspond to the locale identifier (LCID) of the target language. You must also specify the --mode (-m) parameter. Valid options are Import or Overwrite. Import applies translations for strings that have not been translated yet in the dataset. Overwrite, as the name implies, overwrites any existing translations in the dataset. Both, Import and Overwrite add a new culture to the dataset if you import a locale that does not yet exist in the dataset. + +The following command imports German translations from a .csv file called de-DE.csv 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.csv -m Overwrite` + +> Note +> +> MTCmd.exe only imports one translation file at a time. To import multiple languages, run MTCmd.exe in a loop. + +### Fallback to the default locale + +By default, Metadata Translator leaves untranslated strings empty in the translated cultures. This is generally acceptable because Power BI can fall back to the default locale if a translated string cannot be found for a caption, description, or display folder. However, DAX measures using the USERCULTURE() function might require translated strings to be present. In order to accommodate these scenarios, you can set the --fallback-mode option to true so that the command line tool takes the string from the default locale and adds it to the translated culture if the translated string is missing or cannot be found. + +The following command imports German translations from a .csv file called de-DE.csv into an AdventureWorks dataset hosted in Power BI, overwriting any existing German strings in the dataset and applying the strings from the default locale if any translations are missing: + +`MTCmd -cs "powerbi://api.powerbi.com/v1.0/myorg/AdventureWorksSource;initial catalog=AdventureWorks" -if C:\Translations\de-DE.csv -m Overwrite -fm true` + +> Note +> +> Only the command-line tool, MTCmd.exe, supports falling back to the strings of the default locale in import mode. The GUI tool, Metadata Translator.exe, does not provide an option to enable this operation. + +### 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. + +#### Generating static key names instead of random GUIDs + +If you find GUID-based resx files difficult to work with, you can switch to more statically generated key names based on the translated property types and the metadata object names in your dataset. In this case, use the --key-prefix option to specify an arbitrary prefix in ExportResx mode. With a key prefix specified, Metadata Translator generates key names following the format: \#\#\. + +The following command exports all translations from an AdventureWorks dataset hosted in Power BI into resx files in a folder called ExportedTranslations using static key names instead of random GUIDs in the resx files (see also the following screenshot for a generated resx): + +`MTCmd -cs "powerbi://api.powerbi.com/v1.0/myorg/AdventureWorksSource;initial catalog=AdventureWorks" -ef C:\ExportedTranslations -m ExportResx -kp arbitraryPrefix` + +![A resx with static key names instead of random GUIDs](https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/A%20resx%20with%20static%20key%20names%20instead%20of%20random%20GUIDs.png) + +> Note +> +> Because Metadata Translator generates the static key names based on the translated property types and the metadata object names in your dataset, be aware that the static key names change if you re-export your translations into resx files following a rename of metadata objects in the dataset. + +#### 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.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/ExpandoObjectExtensions.cs b/MetadataTranslator/Metadata Translator/Helpers/ExpandoObjectExtensions.cs new file mode 100644 index 0000000..5761539 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Helpers/ExpandoObjectExtensions.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public static class ExpandoObjectExtensions + { + public static string SeparateCamelCase(this ExpandoObject expando, string columnName) + { + if (expando != null && ((IDictionary)expando)[columnName] is string text) + { + char separator = ' '; + char lastChar = separator; + + var sb = new StringBuilder(); + foreach (var currentChar in text.Replace("_", "")) + { + if (char.IsUpper(currentChar) && lastChar != separator) + sb.Append(separator); + + sb.Append(currentChar); + + lastChar = currentChar; + } + + return sb.ToString(); + } + return string.Empty; + } + + public static string GetValue(this ExpandoObject expando, string columnName) + { + return expando.GetObject(columnName)?.ToString(); + } + + public static object GetObject(this ExpandoObject expando, string columnName) + { + if (expando != null && ((IDictionary)expando).ContainsKey(columnName)) + { + return ((IDictionary)expando)[columnName]; + } + return null; + } + + public static void SetValue(this ExpandoObject expando, string columnName, string value, bool overwrite) + { + if (expando != null) + { + if (overwrite || string.IsNullOrEmpty(expando.GetValue(columnName))) + { + ((IDictionary)expando)[columnName] = value; + } + } + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Helpers/Hourglass.cs b/MetadataTranslator/Metadata Translator/Helpers/Hourglass.cs new file mode 100644 index 0000000..7874d25 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Helpers/Hourglass.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Metadata_Translator +{ + public class Hourglass : IDisposable + { + private Cursor previousCursor; + + public Hourglass() + { + previousCursor = Mouse.OverrideCursor; + Mouse.OverrideCursor = Cursors.Wait; + } + + public void Dispose() + { + Mouse.OverrideCursor = previousCursor; + } + } +} \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Helpers/ListExtensions.cs b/MetadataTranslator/Metadata Translator/Helpers/ListExtensions.cs new file mode 100644 index 0000000..6dfc28b --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Helpers/ListExtensions.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Dynamic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public static class ListExtensions + { + public static List GetValues(this List collection, string containerColumnName, string referenceColumnName, string columnName) + { + if (collection == null) return new List(); + + var values = new List(); + foreach (ExpandoObject row in collection) + { + var metaContainer = (MetadataObjectContainer)row.GetObject(containerColumnName); + string refValue = row.GetValue(referenceColumnName); + + if (!string.IsNullOrEmpty(refValue)) + values.Add(new CsvRow { Type = metaContainer.TranslatedProperty.ToString(), Original = refValue, Translation = row.GetValue(columnName) }); + } + return values; + } + + public static Dictionary GetKeyValuePairs(this List collection, string containerColumnName, string columnName, string namePrefix) + { + if (collection == null) return new Dictionary(); + + var values = new Dictionary(); + foreach (ExpandoObject row in collection) + { + var metaContainer = (MetadataObjectContainer)row.GetObject(containerColumnName); + var columnValue = row.GetValue(columnName); + + if (metaContainer != null && !string.IsNullOrEmpty(columnValue)) + { + try + { + values.Add(metaContainer.GetUniqueName(namePrefix), columnValue); + } + catch (Exception ex) + { + throw new Exception(ex.Message + " --- " + metaContainer.TemporaryObjectId.ToString() + metaContainer.ToString()); + } + } + } + return values; + } + } +} 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/Helpers/ObservableCollectionExtensions.cs b/MetadataTranslator/Metadata Translator/Helpers/ObservableCollectionExtensions.cs new file mode 100644 index 0000000..88ca394 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Helpers/ObservableCollectionExtensions.cs @@ -0,0 +1,95 @@ +using Microsoft.AnalysisServices.Tabular; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Dynamic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public static class ObservableCollectionExtensions + { + public static string GetValueAt(this ObservableCollection collection, int index, string columnName) + { + if (collection == null) return string.Empty; + + ExpandoObject row = collection[index]; + return ((IDictionary)row)[columnName]?.ToString(); + } + + public static void SetValueAt(this ObservableCollection collection, int index, string columnName, string value) + { + if (collection == null) return; + + ExpandoObject row = collection[index]; + ((IDictionary)row)[columnName] = value; + } + + public static void UpdateDataValues(this ObservableCollection collection, List sourcePhrases, string sourceLanguage, List targetPhrases, string targetLanguage) + { + if (collection == null) return; + int rowOffset = 0; + for (int i = 0; i < collection.Count; i++) + { + string columnValue = collection.GetValueAt(i, sourceLanguage); + if (!string.IsNullOrEmpty(columnValue) && columnValue.Equals(sourcePhrases[i - rowOffset])) + { + collection.SetValueAt(i, targetLanguage, targetPhrases[i - rowOffset]); + } + else + { + rowOffset++; + } + } + } + + public static List GetValues(this ObservableCollection collection, string columnName) + { + if (collection == null) return new List(); + + List values = new List(); + foreach (ExpandoObject row in collection) + { + string value = ((IDictionary)row)[columnName]?.ToString(); + + if (!string.IsNullOrEmpty(value)) + values.Add(value); + } + return values; + } + + public static void AddDisplayFolder(this ObservableCollection collection, NamedMetadataObject metadataObject, string displayString, string defaultCulture, CultureCollection cultures) + { + if (collection == null) return; + + if (!string.IsNullOrEmpty(displayString)) + { + foreach (ExpandoObject item in collection) + { + if (((IDictionary)item)[defaultCulture] is string displayName && displayName.Equals(displayString)) + { + var existingDisplayFolderContainer = ((IDictionary)item)["Object"] as DisplayFolderContainer; + existingDisplayFolderContainer.TabularObjects.Add(metadataObject); + return; + } + } + } + + dynamic row = new ExpandoObject(); + var displayFolderContainer = new DisplayFolderContainer(metadataObject, TranslatedProperty.DisplayFolder); + + ((IDictionary)row)["Object"] = displayFolderContainer; + foreach (var culture in cultures) + { + ((IDictionary)row)[culture.Name] = culture.Name.Equals(defaultCulture) ? displayString : + culture.ObjectTranslations[displayFolderContainer.TabularObject, displayFolderContainer.TranslatedProperty]?.Value; + } + + collection.Add(row); + } + + } +} diff --git a/MetadataTranslator/Metadata Translator/Helpers/StringExtensions.cs b/MetadataTranslator/Metadata Translator/Helpers/StringExtensions.cs new file mode 100644 index 0000000..794b895 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Helpers/StringExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.AnalysisServices.Tabular; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Dynamic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public static class StringExtensions + { + public static string ToCsvString(this string value) + { + if (value == null) return string.Empty; + return value.Contains("\"") ? $"\"{value.Replace("\"", "\"\"")}\"" : value; + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Installer1.Designer.cs b/MetadataTranslator/Metadata Translator/Installer1.Designer.cs new file mode 100644 index 0000000..febce62 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Installer1.Designer.cs @@ -0,0 +1,37 @@ + +namespace Metadata_Translator +{ + partial class Installer1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + } + + #endregion + } +} \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Installer1.cs b/MetadataTranslator/Metadata Translator/Installer1.cs new file mode 100644 index 0000000..42166e1 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Installer1.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration.Install; +using System.Linq; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Security.Permissions; +using System.Windows; +using System.IO; + +namespace Metadata_Translator +{ + [RunInstaller(true)] + public partial class Installer1 : System.Configuration.Install.Installer + { + public Installer1() + { + InitializeComponent(); + } + + [SecurityPermission(SecurityAction.Demand)] + public override void Commit(System.Collections.IDictionary savedState) + { + base.Commit(savedState); + MessageBox.Show("Commit"); + } + + [SecurityPermission(SecurityAction.Demand)] + public override void Install(System.Collections.IDictionary stateSaver) + { + base.Install(stateSaver); + + string exeName = Context.Parameters["assemblypath"]; + string appDir = Path.GetDirectoryName(exeName); + string pbiSharedDir = Environment.ExpandEnvironmentVariables(@"%CommonProgramFiles%\microsoft shared\Power BI Desktop\External Tools\"); + string pbiToolsJsonTemplate = $"{appDir}\\metadata-translator.pbitool.json"; + if (File.Exists(pbiToolsJsonTemplate)) + { + string pbiToolsJson = File.ReadAllText(pbiToolsJsonTemplate).Replace("", exeName.Replace("\\", "\\\\")); + File.WriteAllText($"{pbiSharedDir}\\metadata-translator.pbitool.json", pbiToolsJson); + } + } + + [SecurityPermission(SecurityAction.Demand)] + public override void Uninstall(System.Collections.IDictionary savedState) + { + base.Uninstall(savedState); + + string pbiSharedDir = Environment.ExpandEnvironmentVariables(@"%CommonProgramFiles%\microsoft shared\Power BI Desktop\External Tools\"); + string pbiToolFile = $"{pbiSharedDir}\\metadata-translator.pbitool.json"; + if (File.Exists(pbiToolFile)) + { + File.Delete(pbiToolFile); + } + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Metadata Translator.csproj b/MetadataTranslator/Metadata Translator/Metadata Translator.csproj new file mode 100644 index 0000000..e3aed8f --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Metadata Translator.csproj @@ -0,0 +1,240 @@ + + + + + Debug + AnyCPU + {CB7D493C-B67E-4438-B304-EFE5D418ADDF} + WinExe + Metadata_Translator + Metadata Translator + v4.7.2 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + icon.ico + + + true + + + MetadataTranslator.snk + + + + ..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.dll + + + ..\packages\Microsoft.AnalysisServices.AdomdClient.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.AdomdClient.dll + + + ..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.Core.dll + + + ..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.SPClient.Interfaces.dll + + + ..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.Tabular.dll + + + ..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.Tabular.Json.dll + + + + + + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + ConnectionStringInput.xaml + + + + + + + + + + + + Component + + + Installer1.cs + + + ImportExportPanel.xaml + + + + LanguagePanel.xaml + + + + SettingsPanel.xaml + + + + TranslationGroupPanel.xaml + + + + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + + MainWindow.xaml + Code + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + Always + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + PreserveNewest + + + + + + + + Always + + + + + False + Microsoft .NET Framework 4.7.2 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + + + + + + \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/MetadataTranslator.snk b/MetadataTranslator/Metadata Translator/MetadataTranslator.snk new file mode 100644 index 0000000..8fd291c Binary files /dev/null and b/MetadataTranslator/Metadata Translator/MetadataTranslator.snk differ diff --git a/MetadataTranslator/Metadata Translator/Properties/AssemblyInfo.cs b/MetadataTranslator/Metadata Translator/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a5cef7f --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Metadata Translator")] +[assembly: AssemblyDescription("Metadata Translator can translate the names and descriptions of the metadata objects in your semantic model by using Azure Cognitive Services.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft Corporation")] +[assembly: AssemblyProduct("Metadata Translator")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// 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.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] diff --git a/MetadataTranslator/Metadata Translator/Properties/Resources.Designer.cs b/MetadataTranslator/Metadata Translator/Properties/Resources.Designer.cs new file mode 100644 index 0000000..8fb55e3 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Properties/Resources.Designer.cs @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +namespace Metadata_Translator.Properties +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Metadata_Translator.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Properties/Resources.resx b/MetadataTranslator/Metadata Translator/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Properties/Settings.Designer.cs b/MetadataTranslator/Metadata Translator/Properties/Settings.Designer.cs new file mode 100644 index 0000000..80cf39c --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Properties/Settings.Designer.cs @@ -0,0 +1,86 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Metadata_Translator.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string SubscriptionKey { + get { + return ((string)(this["SubscriptionKey"])); + } + set { + this["SubscriptionKey"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://api.cognitive.microsofttranslator.com/")] + public string TranslatorEndpoint { + get { + return ((string)(this["TranslatorEndpoint"])); + } + set { + this["TranslatorEndpoint"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string TranslatorLocation { + get { + return ((string)(this["TranslatorLocation"])); + } + set { + this["TranslatorLocation"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OverwriteTranslation { + get { + return ((bool)(this["OverwriteTranslation"])); + } + set { + this["OverwriteTranslation"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string LastUsedExportFolder { + get { + return ((string)(this["LastUsedExportFolder"])); + } + set { + this["LastUsedExportFolder"] = value; + } + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Properties/Settings.settings b/MetadataTranslator/Metadata Translator/Properties/Settings.settings new file mode 100644 index 0000000..1f899dc --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Properties/Settings.settings @@ -0,0 +1,21 @@ + + + + + + + + + https://api.cognitive.microsofttranslator.com/ + + + + + + False + + + + + + \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Resources/StringDictionary.xaml b/MetadataTranslator/Metadata Translator/Resources/StringDictionary.xaml new file mode 100644 index 0000000..5fb7b1b --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Resources/StringDictionary.xaml @@ -0,0 +1,44 @@ + + Languages + Opens the Languages pane to choose the target languages. + Settings + Opens the Settings pane to configure Machine Translator. + Translated Property + Caption + Displays the captions of the metadata objects and their translations. + Description + Displays the descriptions of the metadata objects and their translations.|The metadata objects in the current dataset have no descriptions. Use Power BI Desktop to add descriptions. + DisplayFolder + Displays the display folder names of the metadata objects and their translations.|The current dataset does not have display folders. Use Power BI Desktop to group columns and measures in display folders. + Prepare + Translate + Translates the captions, descriptions, and display folder names using Microsoft Translator. + Apply + Applies the translated captions, descriptions, and display folder names to the Power BI dataset. + Import/Export + Opens the Import/Export pane to export and import the captions, descriptions, and display folder names via .csv files. + Metadata Translator cannot start without server and database arguments. Double-check the 'arguments' setting in the metadata-translator.pbitool.json file in your Microsoft Shared\Power BI Desktop\External Tools folder. The value must be: "\"%server%\" \"%database%\"". + Dataset Connection String + Enter the connection string to the dataset. + Translator Subscription Key + Translator Endpoint + Translator Location + Overwrite Translation + Enter your Microsoft Translator service subscription key. + Enter the Microsoft Translator URL, specifically: https://api.cognitive.microsofttranslator.com/ + This is typically the Global region, but could also be a single Azure region. + Please select at least one language before you translate the metadata. + Microsoft Translator was unable to translate the metadata strings into {0}. Please try again later or remove this language from the translation. + Export .csv files + You can export the metadata captions, descriptions, and display folder strings into .csv files based on the locale identifiers (LCIDs) of the selected languages. Metadata Translator creates a separate .csv file for each LCID. + Select the folder where you want to store your .csv files. Note that Metadata Translator might overwrite any existing files in this folder. + Export... + Import .csv files + You can import translated captions, descriptions, and display folder strings from .csv files. Each file name must match a locale identifier (LCID) of a supported language. You can import multiple .csv files in a single step. + Import... + Object - Type + The default culture is read-only. Use Power BI Desktop to change the names, descriptions, and display folder names for the default culture. + \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Resources/supportedlanguages.json b/MetadataTranslator/Metadata Translator/Resources/supportedlanguages.json new file mode 100644 index 0000000..9724a7f --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Resources/supportedlanguages.json @@ -0,0 +1,2361 @@ +[ + { + "LanguageTag": "af-NA", + "TranslationId": "af", + "TranslationGroup": "Afrikaans", + "DisplayName": "Afrikaans (Namibia)", + "NativeName": "Afrikaans (Namibië)" + }, + { + "LanguageTag": "af-ZA", + "TranslationId": "af", + "TranslationGroup": "Afrikaans", + "DisplayName": "Afrikaans (South Africa)", + "NativeName": "Afrikaans (Suid-Afrika)" + }, + { + "LanguageTag": "bs-Latn-BA", + "TranslationId": "bs", + "TranslationGroup": "Bosnian (Latin)", + "DisplayName": "Bosnian (Latin, Bosnia & Herzegovina)", + "NativeName": "bosanski (Bosna i Hercegovina)" + }, + { + "LanguageTag": "ar-001", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (World)", + "NativeName": "العربية (العالم)" + }, + { + "LanguageTag": "ar-AE", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (United Arab Emirates)", + "NativeName": "العربية (الإمارات العربية المتحدة)" + }, + { + "LanguageTag": "ar-BH", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Bahrain)", + "NativeName": "العربية (البحرين)" + }, + { + "LanguageTag": "ar-DJ", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Djibouti)", + "NativeName": "العربية (جيبوتي)" + }, + { + "LanguageTag": "ar-DZ", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Algeria)", + "NativeName": "العربية (الجزائر)" + }, + { + "LanguageTag": "ar-EG", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Egypt)", + "NativeName": "العربية (مصر)" + }, + { + "LanguageTag": "ar-ER", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Eritrea)", + "NativeName": "العربية (إريتريا)" + }, + { + "LanguageTag": "ar-IL", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Israel)", + "NativeName": "العربية (إسرائيل)" + }, + { + "LanguageTag": "ar-IQ", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Iraq)", + "NativeName": "العربية (العراق)" + }, + { + "LanguageTag": "ar-JO", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Jordan)", + "NativeName": "العربية (الأردن)" + }, + { + "LanguageTag": "ar-KM", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Comoros)", + "NativeName": "العربية (جزر القمر)" + }, + { + "LanguageTag": "ar-KW", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Kuwait)", + "NativeName": "العربية (الكويت)" + }, + { + "LanguageTag": "ar-LB", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Lebanon)", + "NativeName": "العربية (لبنان)" + }, + { + "LanguageTag": "ar-LY", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Libya)", + "NativeName": "العربية (ليبيا)" + }, + { + "LanguageTag": "ar-MA", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Morocco)", + "NativeName": "العربية (المملكة المغربية)" + }, + { + "LanguageTag": "ar-MR", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Mauritania)", + "NativeName": "العربية (موريتانيا)" + }, + { + "LanguageTag": "ar-OM", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Oman)", + "NativeName": "العربية (عمان)" + }, + { + "LanguageTag": "ar-PS", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Palestinian Authority)", + "NativeName": "العربية (السلطة الفلسطينية)" + }, + { + "LanguageTag": "ar-QA", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Qatar)", + "NativeName": "العربية (قطر)" + }, + { + "LanguageTag": "ar-SA", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Saudi Arabia)", + "NativeName": "العربية (المملكة العربية السعودية)" + }, + { + "LanguageTag": "ar-SD", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Sudan)", + "NativeName": "العربية (السودان)" + }, + { + "LanguageTag": "ar-SO", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Somalia)", + "NativeName": "العربية (الصومال)" + }, + { + "LanguageTag": "ar-SS", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (South Sudan)", + "NativeName": "العربية (جنوب السودان)" + }, + { + "LanguageTag": "ar-SY", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Syria)", + "NativeName": "العربية (سوريا)" + }, + { + "LanguageTag": "ar-TD", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Chad)", + "NativeName": "العربية (تشاد)" + }, + { + "LanguageTag": "ar-TN", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Tunisia)", + "NativeName": "العربية (تونس)" + }, + { + "LanguageTag": "ar-YE", + "TranslationId": "ar", + "TranslationGroup": "Arabic", + "DisplayName": "Arabic (Yemen)", + "NativeName": "العربية (اليمن)" + }, + { + "LanguageTag": "as-IN", + "TranslationId": "as", + "TranslationGroup": "Assamese", + "DisplayName": "Assamese (India)", + "NativeName": "অসমীয়া (ভাৰত)" + }, + { + "LanguageTag": "bg-BG", + "TranslationId": "bg", + "TranslationGroup": "Bulgarian", + "DisplayName": "Bulgarian (Bulgaria)", + "NativeName": "български (България)" + }, + { + "LanguageTag": "bn-BD", + "TranslationId": "bn", + "TranslationGroup": "Bangla", + "DisplayName": "Bangla (Bangladesh)", + "NativeName": "বাংলা (বাংলাদেশ)" + }, + { + "LanguageTag": "bn-IN", + "TranslationId": "bn", + "TranslationGroup": "Bangla", + "DisplayName": "Bengali (India)", + "NativeName": "বাংলা (ভারত)" + }, + { + "LanguageTag": "ca-AD", + "TranslationId": "ca", + "TranslationGroup": "Catalan", + "DisplayName": "Catalan (Andorra)", + "NativeName": "català (Andorra)" + }, + { + "LanguageTag": "ca-ES", + "TranslationId": "ca", + "TranslationGroup": "Catalan", + "DisplayName": "Catalan (Catalan)", + "NativeName": "català (català)" + }, + { + "LanguageTag": "ca-FR", + "TranslationId": "ca", + "TranslationGroup": "Catalan", + "DisplayName": "Catalan (France)", + "NativeName": "català (França)" + }, + { + "LanguageTag": "ca-IT", + "TranslationId": "ca", + "TranslationGroup": "Catalan", + "DisplayName": "Catalan (Italy)", + "NativeName": "català (Itàlia)" + }, + { + "LanguageTag": "cs-CZ", + "TranslationId": "cs", + "TranslationGroup": "Czech", + "DisplayName": "Czech (Czechia)", + "NativeName": "čeština (Česko)" + }, + { + "LanguageTag": "da-DK", + "TranslationId": "da", + "TranslationGroup": "Danish", + "DisplayName": "Danish (Denmark)", + "NativeName": "dansk (Danmark)" + }, + { + "LanguageTag": "da-GL", + "TranslationId": "da", + "TranslationGroup": "Danish", + "DisplayName": "Danish (Greenland)", + "NativeName": "dansk (Grønland)" + }, + { + "LanguageTag": "en-001", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (World)", + "NativeName": "English (World)" + }, + { + "LanguageTag": "en-029", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Caribbean)", + "NativeName": "English (Caribbean)" + }, + { + "LanguageTag": "en-150", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Europe)", + "NativeName": "English (Europe)" + }, + { + "LanguageTag": "en-AE", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (United Arab Emirates)", + "NativeName": "English (United Arab Emirates)" + }, + { + "LanguageTag": "en-AG", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Antigua & Barbuda)", + "NativeName": "English (Antigua & Barbuda)" + }, + { + "LanguageTag": "en-AI", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Anguilla)", + "NativeName": "English (Anguilla)" + }, + { + "LanguageTag": "en-AS", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (American Samoa)", + "NativeName": "English (American Samoa)" + }, + { + "LanguageTag": "en-AT", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Austria)", + "NativeName": "English (Austria)" + }, + { + "LanguageTag": "en-AU", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Australia)", + "NativeName": "English (Australia)" + }, + { + "LanguageTag": "en-BB", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Barbados)", + "NativeName": "English (Barbados)" + }, + { + "LanguageTag": "en-BE", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Belgium)", + "NativeName": "English (Belgium)" + }, + { + "LanguageTag": "en-BI", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Burundi)", + "NativeName": "English (Burundi)" + }, + { + "LanguageTag": "en-BM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Bermuda)", + "NativeName": "English (Bermuda)" + }, + { + "LanguageTag": "en-BS", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Bahamas)", + "NativeName": "English (Bahamas)" + }, + { + "LanguageTag": "en-BW", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Botswana)", + "NativeName": "English (Botswana)" + }, + { + "LanguageTag": "en-BZ", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Belize)", + "NativeName": "English (Belize)" + }, + { + "LanguageTag": "en-CA", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Canada)", + "NativeName": "English (Canada)" + }, + { + "LanguageTag": "en-CC", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Cocos (Keeling) Islands)", + "NativeName": "English (Cocos (Keeling) Islands)" + }, + { + "LanguageTag": "en-CH", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Switzerland)", + "NativeName": "English (Switzerland)" + }, + { + "LanguageTag": "en-CK", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Cook Islands)", + "NativeName": "English (Cook Islands)" + }, + { + "LanguageTag": "en-CM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Cameroon)", + "NativeName": "English (Cameroon)" + }, + { + "LanguageTag": "en-CX", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Christmas Island)", + "NativeName": "English (Christmas Island)" + }, + { + "LanguageTag": "en-CY", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Cyprus)", + "NativeName": "English (Cyprus)" + }, + { + "LanguageTag": "en-DE", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Germany)", + "NativeName": "English (Germany)" + }, + { + "LanguageTag": "en-DK", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Denmark)", + "NativeName": "English (Denmark)" + }, + { + "LanguageTag": "en-DM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Dominica)", + "NativeName": "English (Dominica)" + }, + { + "LanguageTag": "en-ER", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Eritrea)", + "NativeName": "English (Eritrea)" + }, + { + "LanguageTag": "en-FI", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Finland)", + "NativeName": "English (Finland)" + }, + { + "LanguageTag": "en-FJ", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Fiji)", + "NativeName": "English (Fiji)" + }, + { + "LanguageTag": "en-FK", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Falkland Islands)", + "NativeName": "English (Falkland Islands)" + }, + { + "LanguageTag": "de-AT", + "TranslationId": "de", + "TranslationGroup": "German", + "DisplayName": "German (Austria)", + "NativeName": "Deutsch (Österreich)" + }, + { + "LanguageTag": "de-BE", + "TranslationId": "de", + "TranslationGroup": "German", + "DisplayName": "German (Belgium)", + "NativeName": "Deutsch (Belgien)" + }, + { + "LanguageTag": "de-CH", + "TranslationId": "de", + "TranslationGroup": "German", + "DisplayName": "German (Switzerland)", + "NativeName": "Deutsch (Schweiz)" + }, + { + "LanguageTag": "de-DE", + "TranslationId": "de", + "TranslationGroup": "German", + "DisplayName": "German (Germany)", + "NativeName": "Deutsch (Deutschland)" + }, + { + "LanguageTag": "de-IT", + "TranslationId": "de", + "TranslationGroup": "German", + "DisplayName": "German (Italy)", + "NativeName": "Deutsch (Italien)" + }, + { + "LanguageTag": "de-LI", + "TranslationId": "de", + "TranslationGroup": "German", + "DisplayName": "German (Liechtenstein)", + "NativeName": "Deutsch (Liechtenstein)" + }, + { + "LanguageTag": "de-LU", + "TranslationId": "de", + "TranslationGroup": "German", + "DisplayName": "German (Luxembourg)", + "NativeName": "Deutsch (Luxemburg)" + }, + { + "LanguageTag": "el-CY", + "TranslationId": "el", + "TranslationGroup": "Greek", + "DisplayName": "Greek (Cyprus)", + "NativeName": "Ελληνικά (Κύπρος)" + }, + { + "LanguageTag": "el-GR", + "TranslationId": "el", + "TranslationGroup": "Greek", + "DisplayName": "Greek (Greece)", + "NativeName": "Ελληνικά (Ελλάδα)" + }, + { + "LanguageTag": "cy-GB", + "TranslationId": "cy", + "TranslationGroup": "Welsh", + "DisplayName": "Welsh (United Kingdom)", + "NativeName": "Cymraeg (Y Deyrnas Unedig)" + }, + { + "LanguageTag": "en-FM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Micronesia)", + "NativeName": "English (Micronesia)" + }, + { + "LanguageTag": "en-GB", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (United Kingdom)", + "NativeName": "English (United Kingdom)" + }, + { + "LanguageTag": "en-GD", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Grenada)", + "NativeName": "English (Grenada)" + }, + { + "LanguageTag": "en-GG", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Guernsey)", + "NativeName": "English (Guernsey)" + }, + { + "LanguageTag": "en-GH", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Ghana)", + "NativeName": "English (Ghana)" + }, + { + "LanguageTag": "en-GI", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Gibraltar)", + "NativeName": "English (Gibraltar)" + }, + { + "LanguageTag": "en-GM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Gambia)", + "NativeName": "English (Gambia)" + }, + { + "LanguageTag": "en-GU", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Guam)", + "NativeName": "English (Guam)" + }, + { + "LanguageTag": "en-GY", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Guyana)", + "NativeName": "English (Guyana)" + }, + { + "LanguageTag": "en-HK", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Hong Kong SAR)", + "NativeName": "English (Hong Kong SAR)" + }, + { + "LanguageTag": "en-ID", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Indonesia)", + "NativeName": "English (Indonesia)" + }, + { + "LanguageTag": "en-IE", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Ireland)", + "NativeName": "English (Ireland)" + }, + { + "LanguageTag": "en-IL", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Israel)", + "NativeName": "English (Israel)" + }, + { + "LanguageTag": "en-IM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Isle of Man)", + "NativeName": "English (Isle of Man)" + }, + { + "LanguageTag": "en-IN", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (India)", + "NativeName": "English (India)" + }, + { + "LanguageTag": "en-IO", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (British Indian Ocean Territory)", + "NativeName": "English (British Indian Ocean Territory)" + }, + { + "LanguageTag": "en-JE", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Jersey)", + "NativeName": "English (Jersey)" + }, + { + "LanguageTag": "en-JM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Jamaica)", + "NativeName": "English (Jamaica)" + }, + { + "LanguageTag": "en-KE", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Kenya)", + "NativeName": "English (Kenya)" + }, + { + "LanguageTag": "en-KI", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Kiribati)", + "NativeName": "English (Kiribati)" + }, + { + "LanguageTag": "en-KN", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (St. Kitts & Nevis)", + "NativeName": "English (St. Kitts & Nevis)" + }, + { + "LanguageTag": "en-KY", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Cayman Islands)", + "NativeName": "English (Cayman Islands)" + }, + { + "LanguageTag": "en-LC", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (St. Lucia)", + "NativeName": "English (St. Lucia)" + }, + { + "LanguageTag": "en-LR", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Liberia)", + "NativeName": "English (Liberia)" + }, + { + "LanguageTag": "en-LS", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Lesotho)", + "NativeName": "English (Lesotho)" + }, + { + "LanguageTag": "en-MG", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Madagascar)", + "NativeName": "English (Madagascar)" + }, + { + "LanguageTag": "en-MH", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Marshall Islands)", + "NativeName": "English (Marshall Islands)" + }, + { + "LanguageTag": "en-MO", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Macao SAR)", + "NativeName": "English (Macao SAR)" + }, + { + "LanguageTag": "en-MP", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Northern Mariana Islands)", + "NativeName": "English (Northern Mariana Islands)" + }, + { + "LanguageTag": "en-MS", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Montserrat)", + "NativeName": "English (Montserrat)" + }, + { + "LanguageTag": "en-MT", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Malta)", + "NativeName": "English (Malta)" + }, + { + "LanguageTag": "en-MU", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Mauritius)", + "NativeName": "English (Mauritius)" + }, + { + "LanguageTag": "en-MW", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Malawi)", + "NativeName": "English (Malawi)" + }, + { + "LanguageTag": "en-MY", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Malaysia)", + "NativeName": "English (Malaysia)" + }, + { + "LanguageTag": "en-NA", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Namibia)", + "NativeName": "English (Namibia)" + }, + { + "LanguageTag": "en-NF", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Norfolk Island)", + "NativeName": "English (Norfolk Island)" + }, + { + "LanguageTag": "en-NG", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Nigeria)", + "NativeName": "English (Nigeria)" + }, + { + "LanguageTag": "en-NL", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Netherlands)", + "NativeName": "English (Netherlands)" + }, + { + "LanguageTag": "en-NR", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Nauru)", + "NativeName": "English (Nauru)" + }, + { + "LanguageTag": "en-NU", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Niue)", + "NativeName": "English (Niue)" + }, + { + "LanguageTag": "en-NZ", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (New Zealand)", + "NativeName": "English (New Zealand)" + }, + { + "LanguageTag": "en-PG", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Papua New Guinea)", + "NativeName": "English (Papua New Guinea)" + }, + { + "LanguageTag": "en-PH", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Philippines)", + "NativeName": "English (Philippines)" + }, + { + "LanguageTag": "en-PK", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Pakistan)", + "NativeName": "English (Pakistan)" + }, + { + "LanguageTag": "en-PN", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Pitcairn Islands)", + "NativeName": "English (Pitcairn Islands)" + }, + { + "LanguageTag": "en-PR", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Puerto Rico)", + "NativeName": "English (Puerto Rico)" + }, + { + "LanguageTag": "en-PW", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Palau)", + "NativeName": "English (Palau)" + }, + { + "LanguageTag": "en-RW", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Rwanda)", + "NativeName": "English (Rwanda)" + }, + { + "LanguageTag": "en-SB", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Solomon Islands)", + "NativeName": "English (Solomon Islands)" + }, + { + "LanguageTag": "en-SC", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Seychelles)", + "NativeName": "English (Seychelles)" + }, + { + "LanguageTag": "en-SD", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Sudan)", + "NativeName": "English (Sudan)" + }, + { + "LanguageTag": "en-SE", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Sweden)", + "NativeName": "English (Sweden)" + }, + { + "LanguageTag": "en-SG", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Singapore)", + "NativeName": "English (Singapore)" + }, + { + "LanguageTag": "en-SH", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (St Helena, Ascension, Tristan da Cunha)", + "NativeName": "English (St Helena, Ascension, Tristan da Cunha)" + }, + { + "LanguageTag": "en-SI", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Slovenia)", + "NativeName": "English (Slovenia)" + }, + { + "LanguageTag": "en-SL", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Sierra Leone)", + "NativeName": "English (Sierra Leone)" + }, + { + "LanguageTag": "en-SS", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (South Sudan)", + "NativeName": "English (South Sudan)" + }, + { + "LanguageTag": "en-SX", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Sint Maarten)", + "NativeName": "English (Sint Maarten)" + }, + { + "LanguageTag": "en-SZ", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Eswatini)", + "NativeName": "English (Eswatini)" + }, + { + "LanguageTag": "en-TC", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Turks & Caicos Islands)", + "NativeName": "English (Turks & Caicos Islands)" + }, + { + "LanguageTag": "en-TK", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Tokelau)", + "NativeName": "English (Tokelau)" + }, + { + "LanguageTag": "en-TO", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Tonga)", + "NativeName": "English (Tonga)" + }, + { + "LanguageTag": "en-TT", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Trinidad & Tobago)", + "NativeName": "English (Trinidad & Tobago)" + }, + { + "LanguageTag": "en-TV", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Tuvalu)", + "NativeName": "English (Tuvalu)" + }, + { + "LanguageTag": "en-TZ", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Tanzania)", + "NativeName": "English (Tanzania)" + }, + { + "LanguageTag": "en-UG", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Uganda)", + "NativeName": "English (Uganda)" + }, + { + "LanguageTag": "en-UM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (U.S. Outlying Islands)", + "NativeName": "English (U.S. Outlying Islands)" + }, + { + "LanguageTag": "en-US", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (United States)", + "NativeName": "English (United States)" + }, + { + "LanguageTag": "en-VC", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (St. Vincent & Grenadines)", + "NativeName": "English (St. Vincent & Grenadines)" + }, + { + "LanguageTag": "en-VG", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (British Virgin Islands)", + "NativeName": "English (British Virgin Islands)" + }, + { + "LanguageTag": "en-VI", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (U.S. Virgin Islands)", + "NativeName": "English (U.S. Virgin Islands)" + }, + { + "LanguageTag": "en-VU", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Vanuatu)", + "NativeName": "English (Vanuatu)" + }, + { + "LanguageTag": "en-WS", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Samoa)", + "NativeName": "English (Samoa)" + }, + { + "LanguageTag": "en-ZA", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (South Africa)", + "NativeName": "English (South Africa)" + }, + { + "LanguageTag": "en-ZM", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Zambia)", + "NativeName": "English (Zambia)" + }, + { + "LanguageTag": "en-ZW", + "TranslationId": "en", + "TranslationGroup": "English", + "DisplayName": "English (Zimbabwe)", + "NativeName": "English (Zimbabwe)" + }, + { + "LanguageTag": "es-419", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Latin America)", + "NativeName": "español (Latinoamérica)" + }, + { + "LanguageTag": "es-AR", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Argentina)", + "NativeName": "español (Argentina)" + }, + { + "LanguageTag": "es-BO", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Bolivia)", + "NativeName": "español (Bolivia)" + }, + { + "LanguageTag": "es-BR", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Brazil)", + "NativeName": "español (Brasil)" + }, + { + "LanguageTag": "es-BZ", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Belize)", + "NativeName": "español (Belice)" + }, + { + "LanguageTag": "es-CL", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Chile)", + "NativeName": "español (Chile)" + }, + { + "LanguageTag": "es-CO", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Colombia)", + "NativeName": "español (Colombia)" + }, + { + "LanguageTag": "es-CR", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Costa Rica)", + "NativeName": "español (Costa Rica)" + }, + { + "LanguageTag": "es-CU", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Cuba)", + "NativeName": "español (Cuba)" + }, + { + "LanguageTag": "es-DO", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Dominican Republic)", + "NativeName": "español (República Dominicana)" + }, + { + "LanguageTag": "es-EC", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Ecuador)", + "NativeName": "español (Ecuador)" + }, + { + "LanguageTag": "es-ES", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Spain, International Sort)", + "NativeName": "español (España, alfabetización internacional)" + }, + { + "LanguageTag": "es-GQ", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Equatorial Guinea)", + "NativeName": "español (Guinea Ecuatorial)" + }, + { + "LanguageTag": "es-GT", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Guatemala)", + "NativeName": "español (Guatemala)" + }, + { + "LanguageTag": "es-HN", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Honduras)", + "NativeName": "español (Honduras)" + }, + { + "LanguageTag": "es-MX", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Mexico)", + "NativeName": "español (México)" + }, + { + "LanguageTag": "es-NI", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Nicaragua)", + "NativeName": "español (Nicaragua)" + }, + { + "LanguageTag": "es-PA", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Panama)", + "NativeName": "español (Panamá)" + }, + { + "LanguageTag": "es-PE", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Peru)", + "NativeName": "español (Perú)" + }, + { + "LanguageTag": "es-PH", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Philippines)", + "NativeName": "español (Filipinas)" + }, + { + "LanguageTag": "es-PR", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Puerto Rico)", + "NativeName": "español (Puerto Rico)" + }, + { + "LanguageTag": "es-PY", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Paraguay)", + "NativeName": "español (Paraguay)" + }, + { + "LanguageTag": "es-SV", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (El Salvador)", + "NativeName": "español (El Salvador)" + }, + { + "LanguageTag": "es-US", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (United States)", + "NativeName": "español (Estados Unidos)" + }, + { + "LanguageTag": "es-UY", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Uruguay)", + "NativeName": "español (Uruguay)" + }, + { + "LanguageTag": "es-VE", + "TranslationId": "es", + "TranslationGroup": "Spanish", + "DisplayName": "Spanish (Venezuela)", + "NativeName": "español (Venezuela)" + }, + { + "LanguageTag": "et-EE", + "TranslationId": "et", + "TranslationGroup": "Estonian", + "DisplayName": "Estonian (Estonia)", + "NativeName": "eesti (Eesti)" + }, + { + "LanguageTag": "fa-IR", + "TranslationId": "fa", + "TranslationGroup": "Persian", + "DisplayName": "Persian (Iran)", + "NativeName": "فارسی (ایران)" + }, + { + "LanguageTag": "fi-FI", + "TranslationId": "fi", + "TranslationGroup": "Finnish", + "DisplayName": "Finnish (Finland)", + "NativeName": "suomi (Suomi)" + }, + { + "LanguageTag": "fil-PH", + "TranslationId": "fil", + "TranslationGroup": "Filipino", + "DisplayName": "Filipino (Philippines)", + "NativeName": "Filipino (Pilipinas)" + }, + { + "LanguageTag": "fr-029", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Caribbean)", + "NativeName": "français (caraïbes)" + }, + { + "LanguageTag": "fr-BE", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Belgium)", + "NativeName": "français (Belgique)" + }, + { + "LanguageTag": "fr-BF", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Burkina Faso)", + "NativeName": "français (Burkina Faso)" + }, + { + "LanguageTag": "fr-BI", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Burundi)", + "NativeName": "français (Burundi)" + }, + { + "LanguageTag": "fr-BJ", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Benin)", + "NativeName": "français (Bénin)" + }, + { + "LanguageTag": "fr-BL", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (St. Barthélemy)", + "NativeName": "français (Saint-Barthélemy)" + }, + { + "LanguageTag": "fr-CA", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Canada)", + "NativeName": "français (Canada)" + }, + { + "LanguageTag": "fr-CD", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French Congo (DRC)", + "NativeName": "français (Congo, République démocratique du)" + }, + { + "LanguageTag": "fr-CF", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Central African Republic)", + "NativeName": "français (République centrafricaine)" + }, + { + "LanguageTag": "fr-CG", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Congo)", + "NativeName": "français (Congo)" + }, + { + "LanguageTag": "fr-CH", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Switzerland)", + "NativeName": "français (Suisse)" + }, + { + "LanguageTag": "fr-CI", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Côte d’Ivoire)", + "NativeName": "français (Côte d’Ivoire)" + }, + { + "LanguageTag": "fr-CM", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Cameroon)", + "NativeName": "français (Cameroun)" + }, + { + "LanguageTag": "fr-DJ", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Djibouti)", + "NativeName": "français (Djibouti)" + }, + { + "LanguageTag": "fr-DZ", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Algeria)", + "NativeName": "français (Algérie)" + }, + { + "LanguageTag": "fr-FR", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (France)", + "NativeName": "français (France)" + }, + { + "LanguageTag": "fr-GA", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Gabon)", + "NativeName": "français (Gabon)" + }, + { + "LanguageTag": "fr-GF", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (French Guiana)", + "NativeName": "français (Guyane française)" + }, + { + "LanguageTag": "fr-GN", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Guinea)", + "NativeName": "français (Guinée)" + }, + { + "LanguageTag": "fr-GP", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Guadeloupe)", + "NativeName": "français (Guadeloupe)" + }, + { + "LanguageTag": "fr-GQ", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Equatorial Guinea)", + "NativeName": "français (Guinée équatoriale)" + }, + { + "LanguageTag": "fr-HT", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Haiti)", + "NativeName": "français (Haïti)" + }, + { + "LanguageTag": "fr-KM", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Comoros)", + "NativeName": "français (Comores)" + }, + { + "LanguageTag": "fr-LU", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Luxembourg)", + "NativeName": "français (Luxembourg)" + }, + { + "LanguageTag": "fr-MA", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Morocco)", + "NativeName": "français (Maroc)" + }, + { + "LanguageTag": "fr-MC", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Monaco)", + "NativeName": "français (Monaco)" + }, + { + "LanguageTag": "fr-MF", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (St. Martin)", + "NativeName": "français (Saint-Martin)" + }, + { + "LanguageTag": "fr-MG", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Madagascar)", + "NativeName": "français (Madagascar)" + }, + { + "LanguageTag": "fr-ML", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Mali)", + "NativeName": "français (Mali)" + }, + { + "LanguageTag": "fr-MQ", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Martinique)", + "NativeName": "français (Martinique)" + }, + { + "LanguageTag": "fr-MR", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Mauritania)", + "NativeName": "français (Mauritanie)" + }, + { + "LanguageTag": "fr-MU", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Mauritius)", + "NativeName": "français (Maurice)" + }, + { + "LanguageTag": "fr-NC", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (New Caledonia)", + "NativeName": "français (Nouvelle-Calédonie)" + }, + { + "LanguageTag": "fr-NE", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Niger)", + "NativeName": "français (Niger)" + }, + { + "LanguageTag": "fr-PF", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (French Polynesia)", + "NativeName": "français (Polynésie française)" + }, + { + "LanguageTag": "fr-PM", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (St. Pierre & Miquelon)", + "NativeName": "français (Saint-Pierre-et-Miquelon)" + }, + { + "LanguageTag": "fr-RE", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Réunion)", + "NativeName": "français (La Réunion)" + }, + { + "LanguageTag": "fr-RW", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Rwanda)", + "NativeName": "français (Rwanda)" + }, + { + "LanguageTag": "fr-SC", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Seychelles)", + "NativeName": "français (Seychelles)" + }, + { + "LanguageTag": "fr-SN", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Senegal)", + "NativeName": "français (Sénégal)" + }, + { + "LanguageTag": "fr-SY", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Syria)", + "NativeName": "français (Syrie)" + }, + { + "LanguageTag": "fr-TD", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Chad)", + "NativeName": "français (Tchad)" + }, + { + "LanguageTag": "fr-TG", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Togo)", + "NativeName": "français (Togo)" + }, + { + "LanguageTag": "fr-TN", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Tunisia)", + "NativeName": "français (Tunisie)" + }, + { + "LanguageTag": "fr-VU", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Vanuatu)", + "NativeName": "français (Vanuatu)" + }, + { + "LanguageTag": "fr-WF", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Wallis & Futuna)", + "NativeName": "français (Wallis-et-Futuna)" + }, + { + "LanguageTag": "fr-YT", + "TranslationId": "fr", + "TranslationGroup": "French", + "DisplayName": "French (Mayotte)", + "NativeName": "français (Mayotte)" + }, + { + "LanguageTag": "ga-IE", + "TranslationId": "ga", + "TranslationGroup": "Irish", + "DisplayName": "Irish (Ireland)", + "NativeName": "Gaeilge (Éire)" + }, + { + "LanguageTag": "gu-IN", + "TranslationId": "gu", + "TranslationGroup": "Gujarati", + "DisplayName": "Gujarati (India)", + "NativeName": "ગુજરાતી (ભારત)" + }, + { + "LanguageTag": "he-IL", + "TranslationId": "he", + "TranslationGroup": "Hebrew", + "DisplayName": "Hebrew (Israel)", + "NativeName": "עברית (ישראל)" + }, + { + "LanguageTag": "hi-IN", + "TranslationId": "hi", + "TranslationGroup": "Hindi", + "DisplayName": "Hindi (India)", + "NativeName": "हिन्दी (भारत)" + }, + { + "LanguageTag": "hr-BA", + "TranslationId": "hr", + "TranslationGroup": "Croatian", + "DisplayName": "Croatian (Bosnia & Herzegovina)", + "NativeName": "hrvatski (Bosna i Hercegovina)" + }, + { + "LanguageTag": "hr-HR", + "TranslationId": "hr", + "TranslationGroup": "Croatian", + "DisplayName": "Croatian (Croatia)", + "NativeName": "hrvatski (Hrvatska)" + }, + { + "LanguageTag": "hu-HU", + "TranslationId": "hu", + "TranslationGroup": "Hungarian", + "DisplayName": "Hungarian (Hungary)", + "NativeName": "magyar (Magyarország)" + }, + { + "LanguageTag": "id-ID", + "TranslationId": "id", + "TranslationGroup": "Indonesian", + "DisplayName": "Indonesian (Indonesia)", + "NativeName": "Indonesia (Indonesia)" + }, + { + "LanguageTag": "is-IS", + "TranslationId": "is", + "TranslationGroup": "Icelandic", + "DisplayName": "Icelandic (Iceland)", + "NativeName": "íslenska (Ísland)" + }, + { + "LanguageTag": "it-CH", + "TranslationId": "it", + "TranslationGroup": "Italian", + "DisplayName": "Italian (Switzerland)", + "NativeName": "italiano (Svizzera)" + }, + { + "LanguageTag": "it-IT", + "TranslationId": "it", + "TranslationGroup": "Italian", + "DisplayName": "Italian (Italy)", + "NativeName": "italiano (Italia)" + }, + { + "LanguageTag": "it-SM", + "TranslationId": "it", + "TranslationGroup": "Italian", + "DisplayName": "Italian (San Marino)", + "NativeName": "italiano (San Marino)" + }, + { + "LanguageTag": "it-VA", + "TranslationId": "it", + "TranslationGroup": "Italian", + "DisplayName": "Italian (Vatican City)", + "NativeName": "italiano (Città del Vaticano)" + }, + { + "LanguageTag": "iu-Cans-CA", + "TranslationId": "iu", + "TranslationGroup": "Inuktitut", + "DisplayName": "Inuktitut (Syllabics, Canada)", + "NativeName": "ᐃᓄᒃᑎᑐᑦ (ᑲᓇᑕᒥ)" + }, + { + "LanguageTag": "ja-JP", + "TranslationId": "ja", + "TranslationGroup": "Japanese", + "DisplayName": "Japanese (Japan)", + "NativeName": "日本語 (日本)" + }, + { + "LanguageTag": "kk-KZ", + "TranslationId": "kk", + "TranslationGroup": "Kazakh", + "DisplayName": "Kazakh (Kazakhstan)", + "NativeName": "қазақ тілі (Қазақстан)" + }, + { + "LanguageTag": "kn-IN", + "TranslationId": "kn", + "TranslationGroup": "Kannada", + "DisplayName": "Kannada (India)", + "NativeName": "ಕನ್ನಡ (ಭಾರತ)" + }, + { + "LanguageTag": "ko-KP", + "TranslationId": "ko", + "TranslationGroup": "Korean", + "DisplayName": "Korean (North Korea)", + "NativeName": "한국어 (조선민주주의인민공화국)" + }, + { + "LanguageTag": "ko-KR", + "TranslationId": "ko", + "TranslationGroup": "Korean", + "DisplayName": "Korean (Korea)", + "NativeName": "한국어(대한민국)" + }, + { + "LanguageTag": "ku-Arab-IQ", + "TranslationId": "ku", + "TranslationGroup": "Kurdish (Central)", + "DisplayName": "Central Kurdish (Iraq)", + "NativeName": "کوردیی ناوەڕاست (عێراق)" + }, + { + "LanguageTag": "ku-Arab-IR", + "TranslationId": "ku", + "TranslationGroup": "Kurdish (Central)", + "DisplayName": "Kurdish (Perso-Arabic, Iran)", + "NativeName": "کوردی (ئێران)" + }, + { + "LanguageTag": "lt-LT", + "TranslationId": "lt", + "TranslationGroup": "Lithuanian", + "DisplayName": "Lithuanian (Lithuania)", + "NativeName": "lietuvių (Lietuva)" + }, + { + "LanguageTag": "lv-LV", + "TranslationId": "lv", + "TranslationGroup": "Latvian", + "DisplayName": "Latvian (Latvia)", + "NativeName": "latviešu (Latvija)" + }, + { + "LanguageTag": "mg-MG", + "TranslationId": "mg", + "TranslationGroup": "Malagasy", + "DisplayName": "Malagasy (Madagascar)", + "NativeName": "Malagasy (Madagasikara)" + }, + { + "LanguageTag": "mi-NZ", + "TranslationId": "mi", + "TranslationGroup": "Maori", + "DisplayName": "Maori (New Zealand)", + "NativeName": "te reo Māori (Aotearoa)" + }, + { + "LanguageTag": "ml-IN", + "TranslationId": "ml", + "TranslationGroup": "Malayalam", + "DisplayName": "Malayalam (India)", + "NativeName": "മലയാളം (ഇന്ത്യ)" + }, + { + "LanguageTag": "mr-IN", + "TranslationId": "mr", + "TranslationGroup": "Marathi", + "DisplayName": "Marathi (India)", + "NativeName": "मराठी (भारत)" + }, + { + "LanguageTag": "ms-BN", + "TranslationId": "ms", + "TranslationGroup": "Malay", + "DisplayName": "Malay (Brunei)", + "NativeName": "Melayu (Brunei)" + }, + { + "LanguageTag": "ms-MY", + "TranslationId": "ms", + "TranslationGroup": "Malay", + "DisplayName": "Malay (Malaysia)", + "NativeName": "Melayu (Malaysia)" + }, + { + "LanguageTag": "ms-SG", + "TranslationId": "ms", + "TranslationGroup": "Malay", + "DisplayName": "Malay (Singapore)", + "NativeName": "Melayu (Singapura)" + }, + { + "LanguageTag": "mt-MT", + "TranslationId": "mt", + "TranslationGroup": "Maltese", + "DisplayName": "Maltese (Malta)", + "NativeName": "Malti (Malta)" + }, + { + "LanguageTag": "nb-NO", + "TranslationId": "nb", + "TranslationGroup": "Norwegian", + "DisplayName": "Norwegian Bokmål (Norway)", + "NativeName": "norsk bokmål (Norge)" + }, + { + "LanguageTag": "nb-SJ", + "TranslationId": "nb", + "TranslationGroup": "Norwegian", + "DisplayName": "Norwegian Bokmål (Svalbard & Jan Mayen)", + "NativeName": "norsk bokmål (Svalbard og Jan Mayen)" + }, + { + "LanguageTag": "nl-AW", + "TranslationId": "nl", + "TranslationGroup": "Dutch", + "DisplayName": "Dutch (Aruba)", + "NativeName": "Nederlands (Aruba)" + }, + { + "LanguageTag": "nl-BE", + "TranslationId": "nl", + "TranslationGroup": "Dutch", + "DisplayName": "Dutch (Belgium)", + "NativeName": "Nederlands (België)" + }, + { + "LanguageTag": "nl-BQ", + "TranslationId": "nl", + "TranslationGroup": "Dutch", + "DisplayName": "Dutch (Bonaire, Sint Eustatius and Saba)", + "NativeName": "Nederlands (Bonaire, Sint Eustatius en Saba)" + }, + { + "LanguageTag": "nl-CW", + "TranslationId": "nl", + "TranslationGroup": "Dutch", + "DisplayName": "Dutch (Curaçao)", + "NativeName": "Nederlands (Curaçao)" + }, + { + "LanguageTag": "nl-NL", + "TranslationId": "nl", + "TranslationGroup": "Dutch", + "DisplayName": "Dutch (Netherlands)", + "NativeName": "Nederlands (Nederland)" + }, + { + "LanguageTag": "nl-SR", + "TranslationId": "nl", + "TranslationGroup": "Dutch", + "DisplayName": "Dutch (Suriname)", + "NativeName": "Nederlands (Suriname)" + }, + { + "LanguageTag": "nl-SX", + "TranslationId": "nl", + "TranslationGroup": "Dutch", + "DisplayName": "Dutch (Sint Maarten)", + "NativeName": "Nederlands (Sint-Maarten)" + }, + { + "LanguageTag": "or-IN", + "TranslationId": "or", + "TranslationGroup": "Odia", + "DisplayName": "Odia (India)", + "NativeName": "ଓଡ଼ିଆ (ଭାରତ)" + }, + { + "LanguageTag": "pa-IN", + "TranslationId": "pa", + "TranslationGroup": "Punjabi", + "DisplayName": "Punjabi (India)", + "NativeName": "ਪੰਜਾਬੀ (ਭਾਰਤ)" + }, + { + "LanguageTag": "pl-PL", + "TranslationId": "pl", + "TranslationGroup": "Polish", + "DisplayName": "Polish (Poland)", + "NativeName": "polski (Polska)" + }, + { + "LanguageTag": "prs-AF", + "TranslationId": "prs", + "TranslationGroup": "Dari", + "DisplayName": "Dari (Afghanistan)", + "NativeName": "درى (افغانستان)" + }, + { + "LanguageTag": "ps-AF", + "TranslationId": "ps", + "TranslationGroup": "Pashto", + "DisplayName": "Pashto (Afghanistan)", + "NativeName": "پښتو (افغانستان)" + }, + { + "LanguageTag": "ps-PK", + "TranslationId": "ps", + "TranslationGroup": "Pashto", + "DisplayName": "Pashto (Pakistan)", + "NativeName": "پښتو (پاکستان)" + }, + { + "LanguageTag": "pt-AO", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Angola)", + "NativeName": "português (Angola)" + }, + { + "LanguageTag": "pt-BR", + "TranslationId": "pt", + "TranslationGroup": "Portuguese (Brazil)", + "DisplayName": "Portuguese (Brazil)", + "NativeName": "português (Brasil)" + }, + { + "LanguageTag": "pt-CH", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Switzerland)", + "NativeName": "português (Suíça)" + }, + { + "LanguageTag": "pt-CV", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Cabo Verde)", + "NativeName": "português (Cabo Verde)" + }, + { + "LanguageTag": "pt-GQ", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Equatorial Guinea)", + "NativeName": "português (Guiné Equatorial)" + }, + { + "LanguageTag": "pt-GW", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Guinea-Bissau)", + "NativeName": "português (Guiné-Bissau)" + }, + { + "LanguageTag": "pt-LU", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Luxembourg)", + "NativeName": "português (Luxemburgo)" + }, + { + "LanguageTag": "pt-MO", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Macao SAR)", + "NativeName": "português (RAE de Macau)" + }, + { + "LanguageTag": "pt-MZ", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Mozambique)", + "NativeName": "português (Moçambique)" + }, + { + "LanguageTag": "pt-PT", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Portugal)", + "NativeName": "português (Portugal)" + }, + { + "LanguageTag": "pt-ST", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (São Tomé & Príncipe)", + "NativeName": "português (São Tomé e Príncipe)" + }, + { + "LanguageTag": "pt-TL", + "TranslationId": "pt-pt", + "TranslationGroup": "Portuguese", + "DisplayName": "Portuguese (Timor-Leste)", + "NativeName": "português (Timor-Leste)" + }, + { + "LanguageTag": "ro-MD", + "TranslationId": "ro", + "TranslationGroup": "Romanian", + "DisplayName": "Romanian (Moldova)", + "NativeName": "română (Republica Moldova)" + }, + { + "LanguageTag": "ro-RO", + "TranslationId": "ro", + "TranslationGroup": "Romanian", + "DisplayName": "Romanian (Romania)", + "NativeName": "română (România)" + }, + { + "LanguageTag": "ru-BY", + "TranslationId": "ru", + "TranslationGroup": "Russian", + "DisplayName": "Russian (Belarus)", + "NativeName": "русский (Беларусь)" + }, + { + "LanguageTag": "ru-KG", + "TranslationId": "ru", + "TranslationGroup": "Russian", + "DisplayName": "Russian (Kyrgyzstan)", + "NativeName": "русский (Киргизия)" + }, + { + "LanguageTag": "ru-KZ", + "TranslationId": "ru", + "TranslationGroup": "Russian", + "DisplayName": "Russian (Kazakhstan)", + "NativeName": "русский (Казахстан)" + }, + { + "LanguageTag": "ru-MD", + "TranslationId": "ru", + "TranslationGroup": "Russian", + "DisplayName": "Russian (Moldova)", + "NativeName": "русский (Молдова)" + }, + { + "LanguageTag": "ru-RU", + "TranslationId": "ru", + "TranslationGroup": "Russian", + "DisplayName": "Russian (Russia)", + "NativeName": "русский (Россия)" + }, + { + "LanguageTag": "ru-UA", + "TranslationId": "ru", + "TranslationGroup": "Russian", + "DisplayName": "Russian (Ukraine)", + "NativeName": "русский (Украина)" + }, + { + "LanguageTag": "sk-SK", + "TranslationId": "sk", + "TranslationGroup": "Slovak", + "DisplayName": "Slovak (Slovakia)", + "NativeName": "slovenčina (Slovensko)" + }, + { + "LanguageTag": "sl-SI", + "TranslationId": "sl", + "TranslationGroup": "Slovenian", + "DisplayName": "Slovenian (Slovenia)", + "NativeName": "slovenščina (Slovenija)" + }, + { + "LanguageTag": "sr-Cyrl-BA", + "TranslationId": "sr-Cyrl", + "TranslationGroup": "Serbian (Cyrillic)", + "DisplayName": "Serbian (Cyrillic, Bosnia and Herzegovina)", + "NativeName": "српски (Босна и Херцеговина)" + }, + { + "LanguageTag": "sr-Cyrl-ME", + "TranslationId": "sr-Cyrl", + "TranslationGroup": "Serbian (Cyrillic)", + "DisplayName": "Serbian (Cyrillic, Montenegro)", + "NativeName": "српски (Црна Гора)" + }, + { + "LanguageTag": "sr-Cyrl-RS", + "TranslationId": "sr-Cyrl", + "TranslationGroup": "Serbian (Cyrillic)", + "DisplayName": "Serbian (Cyrillic, Serbia)", + "NativeName": "српски (Србија)" + }, + { + "LanguageTag": "sr-Cyrl-XK", + "TranslationId": "sr-Cyrl", + "TranslationGroup": "Serbian (Cyrillic)", + "DisplayName": "Serbian (Cyrillic, Kosovo)", + "NativeName": "српски (Косово)" + }, + { + "LanguageTag": "sr-Latn-BA", + "TranslationId": "sr-Latn", + "TranslationGroup": "Serbian (Latin)", + "DisplayName": "Serbian (Latin, Bosnia & Herzegovina)", + "NativeName": "srpski (Bosna i Hercegovina)" + }, + { + "LanguageTag": "sr-Latn-ME", + "TranslationId": "sr-Latn", + "TranslationGroup": "Serbian (Latin)", + "DisplayName": "Serbian (Latin, Montenegro)", + "NativeName": "srpski (Crna Gora)" + }, + { + "LanguageTag": "sr-Latn-RS", + "TranslationId": "sr-Latn", + "TranslationGroup": "Serbian (Latin)", + "DisplayName": "Serbian (Latin, Serbia)", + "NativeName": "srpski (Srbija)" + }, + { + "LanguageTag": "sr-Latn-XK", + "TranslationId": "sr-Latn", + "TranslationGroup": "Serbian (Latin)", + "DisplayName": "Serbian (Latin, Kosovo)", + "NativeName": "srpski (Kosovo)" + }, + { + "LanguageTag": "sv-AX", + "TranslationId": "sv", + "TranslationGroup": "Swedish", + "DisplayName": "Swedish (Åland Islands)", + "NativeName": "svenska (Åland)" + }, + { + "LanguageTag": "sv-FI", + "TranslationId": "sv", + "TranslationGroup": "Swedish", + "DisplayName": "Swedish (Finland)", + "NativeName": "svenska (Finland)" + }, + { + "LanguageTag": "sv-SE", + "TranslationId": "sv", + "TranslationGroup": "Swedish", + "DisplayName": "Swedish (Sweden)", + "NativeName": "svenska (Sverige)" + }, + { + "LanguageTag": "sw-CD", + "TranslationId": "sw", + "TranslationGroup": "Swahili", + "DisplayName": "Kiswahili (Congo DRC)", + "NativeName": "Kiswahili (Jamhuri ya Kidemokrasia ya Kongo)" + }, + { + "LanguageTag": "sw-KE", + "TranslationId": "sw", + "TranslationGroup": "Swahili", + "DisplayName": "Kiswahili (Kenya)", + "NativeName": "Kiswahili (Kenya)" + }, + { + "LanguageTag": "sw-TZ", + "TranslationId": "sw", + "TranslationGroup": "Swahili", + "DisplayName": "Kiswahili (Tanzania)", + "NativeName": "Kiswahili (Tanzania)" + }, + { + "LanguageTag": "sw-UG", + "TranslationId": "sw", + "TranslationGroup": "Swahili", + "DisplayName": "Kiswahili (Uganda)", + "NativeName": "Kiswahili (Uganda)" + }, + { + "LanguageTag": "ta-IN", + "TranslationId": "ta", + "TranslationGroup": "Tamil", + "DisplayName": "Tamil (India)", + "NativeName": "தமிழ் (இந்தியா)" + }, + { + "LanguageTag": "ta-LK", + "TranslationId": "ta", + "TranslationGroup": "Tamil", + "DisplayName": "Tamil (Sri Lanka)", + "NativeName": "தமிழ் (இலங்கை)" + }, + { + "LanguageTag": "ta-MY", + "TranslationId": "ta", + "TranslationGroup": "Tamil", + "DisplayName": "Tamil (Malaysia)", + "NativeName": "தமிழ் (மலேசியா)" + }, + { + "LanguageTag": "ta-SG", + "TranslationId": "ta", + "TranslationGroup": "Tamil", + "DisplayName": "Tamil (Singapore)", + "NativeName": "தமிழ் (சிங்கப்பூர்)" + }, + { + "LanguageTag": "te-IN", + "TranslationId": "te", + "TranslationGroup": "Telugu", + "DisplayName": "Telugu (India)", + "NativeName": "తెలుగు (భారతదేశం)" + }, + { + "LanguageTag": "th-TH", + "TranslationId": "th", + "TranslationGroup": "Thai", + "DisplayName": "Thai (Thailand)", + "NativeName": "ไทย (ไทย)" + }, + { + "LanguageTag": "to-TO", + "TranslationId": "to", + "TranslationGroup": "Tongan", + "DisplayName": "Tongan (Tonga)", + "NativeName": "lea fakatonga (Tonga)" + }, + { + "LanguageTag": "tr-CY", + "TranslationId": "tr", + "TranslationGroup": "Turkish", + "DisplayName": "Turkish (Cyprus)", + "NativeName": "Türkçe (Kıbrıs)" + }, + { + "LanguageTag": "tr-TR", + "TranslationId": "tr", + "TranslationGroup": "Turkish", + "DisplayName": "Turkish (Turkey)", + "NativeName": "Türkçe (Türkiye)" + }, + { + "LanguageTag": "uk-UA", + "TranslationId": "uk", + "TranslationGroup": "Ukrainian", + "DisplayName": "Ukrainian (Ukraine)", + "NativeName": "українська (Україна)" + }, + { + "LanguageTag": "ur-IN", + "TranslationId": "ur", + "TranslationGroup": "Urdu", + "DisplayName": "Urdu (India)", + "NativeName": "اردو (بھارت)" + }, + { + "LanguageTag": "ur-PK", + "TranslationId": "ur", + "TranslationGroup": "Urdu", + "DisplayName": "Urdu (Pakistan)", + "NativeName": "اُردو (پاکستان)" + }, + { + "LanguageTag": "vi-VN", + "TranslationId": "vi", + "TranslationGroup": "Vietnamese", + "DisplayName": "Vietnamese (Vietnam)", + "NativeName": "Tiếng Việt (Việt Nam)" + }, + { + "LanguageTag": "zh-CN", + "TranslationId": "zh-Hans", + "TranslationGroup": "Chinese (Simplified)", + "DisplayName": "Chinese (Simplified, China)", + "NativeName": "中文(中国)" + }, + { + "LanguageTag": "zh-Hans-HK", + "TranslationId": "zh-Hans", + "TranslationGroup": "Chinese (Simplified)", + "DisplayName": "Chinese (Simplified Han, Hong Kong SAR)", + "NativeName": "中文 (香港特别行政区)" + }, + { + "LanguageTag": "zh-Hans-MO", + "TranslationId": "zh-Hans", + "TranslationGroup": "Chinese (Simplified)", + "DisplayName": "Chinese (Simplified Han, Macao SAR)", + "NativeName": "中文 (澳门特别行政区)" + }, + { + "LanguageTag": "zh-HK", + "TranslationId": "zh-Hant", + "TranslationGroup": "Chinese (Traditional)", + "DisplayName": "Chinese (Traditional, Hong Kong SAR)", + "NativeName": "中文(香港特別行政區)" + }, + { + "LanguageTag": "zh-MO", + "TranslationId": "zh-Hant", + "TranslationGroup": "Chinese (Traditional)", + "DisplayName": "Chinese (Traditional, Macao SAR)", + "NativeName": "中文(澳門特別行政區)" + }, + { + "LanguageTag": "zh-SG", + "TranslationId": "zh-Hans", + "TranslationGroup": "Chinese (Simplified)", + "DisplayName": "Chinese (Simplified, Singapore)", + "NativeName": "中文(新加坡)" + }, + { + "LanguageTag": "zh-TW", + "TranslationId": "zh-Hant", + "TranslationGroup": "Chinese (Traditional)", + "DisplayName": "Chinese (Traditional, Taiwan)", + "NativeName": "中文(台灣)" + } +] \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Translations/Language.cs b/MetadataTranslator/Metadata Translator/Translations/Language.cs new file mode 100644 index 0000000..9cde8a9 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Translations/Language.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public class Language : INotifyPropertyChanged + { + #region INotifyPropertyChanged implementation + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void NotifyPropertyChanged(string propertyName = null) + { + if (PropertyChanged != null) + PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + #endregion + + public string LanguageTag { get; set; } + public string TranslationId { get; set; } + public string TranslationGroup { get; set; } + public string DisplayName { get; set; } + public string NativeName { get; set; } + + private bool isSelected = false; + public bool IsSelected + { + get => isSelected; + set + { + isSelected = value; + NotifyPropertyChanged("IsSelected"); + } + } + + public bool IsModelDefault { get; set; } + public bool IsNotModelDefault { get => !IsModelDefault; } + } +} \ No newline at end of file diff --git a/MetadataTranslator/Metadata Translator/Translations/Translation.cs b/MetadataTranslator/Metadata Translator/Translations/Translation.cs new file mode 100644 index 0000000..78adcf0 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Translations/Translation.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public class Translation + { + public string text { get; set; } + public string to { get; set; } + } +} diff --git a/MetadataTranslator/Metadata Translator/Translations/TranslationGroup.cs b/MetadataTranslator/Metadata Translator/Translations/TranslationGroup.cs new file mode 100644 index 0000000..73102b3 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Translations/TranslationGroup.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public class TranslationGroup + { + public string Name { get; set; } + public string Tag { get; set; } + + public override bool Equals(object obj) + { + if (obj is TranslationGroup comparewith) + { + return Name == comparewith.Name && Tag == comparewith.Tag; + } + return false; + } + + public override int GetHashCode() + { + return new { Tag, Name }.GetHashCode(); + } + } +} diff --git a/MetadataTranslator/Metadata Translator/Translations/TranslationResult.cs b/MetadataTranslator/Metadata Translator/Translations/TranslationResult.cs new file mode 100644 index 0000000..0e17b4b --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Translations/TranslationResult.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Metadata_Translator +{ + public class TranslationResult + { + public List translations { get; set; } + } +} diff --git a/MetadataTranslator/Metadata Translator/Translations/TranslatorService.cs b/MetadataTranslator/Metadata Translator/Translations/TranslatorService.cs new file mode 100644 index 0000000..a109c0c --- /dev/null +++ b/MetadataTranslator/Metadata Translator/Translations/TranslatorService.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Dynamic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web.Script.Serialization; + +namespace Metadata_Translator +{ + public class TranslatorService + { + List Languages { get; set; } + string SourceLanguage { get; set; } + string SubscriptionKey { get; set; } + string Endpoint { get; set; } + string Location { get; set; } + + /// + /// + /// + /// + /// + /// + /// + /// + public TranslatorService(List languages, string sourceLanguage, string subscriptionKey, string endpoint, string location) + { + Languages = languages; + SourceLanguage = sourceLanguage; + + SubscriptionKey = subscriptionKey; + Endpoint = endpoint; + Location = location; + } + + public void Translate(List dataRows, bool replaceExistingTranslations) + { + /// No languages, or only the one source language? Nothing to translate! + /// + if (Languages == null || Languages.Count < 2) return; + + /// Get the target languages (i.e. not the SourceLanguage). + /// + List targetLanguages = Languages.Where(l => !l.LanguageTag.Equals(SourceLanguage)).ToList(); + + /// Filter down the data rows to those with values in the source language. + /// + List filteredRows = dataRows.Where(dr => !string.IsNullOrEmpty(dr.GetValue(SourceLanguage)))?.ToList(); + + /// No rows? Nothing to translate! + /// + if (filteredRows == null || filteredRows.Count == 0) return; + + /// Iterate over the TranslationGroups, all languages within the same group + /// share the same translation id. + /// + foreach(string id in targetLanguages.Select(tl => tl.TranslationId).Distinct()) + { + Translate(filteredRows, targetLanguages.Where(tl => tl.TranslationId.Equals(id)), id, replaceExistingTranslations); + } + } + + private void Translate(List dataRows, IEnumerable targetLanguages, string translationId, bool replaceExistingTranslations) + { + List rowsToTranslate = new List(); + + /// Don't replace existing translations? + /// Filter down the data rows to those that are empty for at least one of the target languages. + /// + if (!replaceExistingTranslations) + { + foreach(Language language in targetLanguages) + { + rowsToTranslate.AddRange(dataRows.Where(dr => string.IsNullOrEmpty(dr.GetValue(language.LanguageTag)))); + } + rowsToTranslate = rowsToTranslate.Distinct()?.ToList(); + } + else + { + /// Otherwise translate all data rows. + /// + rowsToTranslate = dataRows; + } + + /// Now translate the source strings recursively. + /// + Translate(rowsToTranslate, targetLanguages, translationId, replaceExistingTranslations, 0); + } + + private void Translate(List dataRows, IEnumerable targetLanguages, string translationId, bool replaceExistingTranslations, int iterationId) + { + int maxBatchSize = 100; + int batchStart = maxBatchSize * iterationId; + + /// Check if all strings have been translated. + /// + if (dataRows.Count <= batchStart) return; + + /// Assemble a translation batch of up to maxBatchSize. + /// + maxBatchSize = (dataRows.Count - batchStart) < maxBatchSize? dataRows.Count - batchStart : maxBatchSize; + List translationBatch = new List(); + for (int i = 0; i < maxBatchSize; i++) + { + translationBatch.Add(new { Text = dataRows[batchStart + i].GetValue(SourceLanguage) }); + } + + /// Translate the batch and assign the translated strings to the target languages. + /// + var translatedStrings = TranslateBatch(translationBatch, translationId); + for (int i = 0; i < maxBatchSize; i++) + { + foreach (Language language in targetLanguages) + { + dataRows[batchStart + i].SetValue(language.LanguageTag, translatedStrings[i], replaceExistingTranslations); + } + } + + Translate(dataRows, targetLanguages, translationId, replaceExistingTranslations, ++iterationId); + } + + private List TranslateBatch(List sourceObjects, string targetLanguage) + { + List translatedPhrases = new List(); + + var requestBody = new JavaScriptSerializer().Serialize(sourceObjects); + using (var client = new HttpClient()) + using (var request = new HttpRequestMessage()) + { + /// Build the Web request. + /// + request.Method = HttpMethod.Post; + request.RequestUri = new Uri($"{Endpoint}/translate?api-version=3.0&from={SourceLanguage}&to={targetLanguage}"); + request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + request.Headers.Add("Ocp-Apim-Subscription-Key", SubscriptionKey); + request.Headers.Add("Ocp-Apim-Subscription-Region", Location); + + /// Send the translation request and get the response. + /// + HttpResponseMessage response = client.SendAsync(request).Result; + string result = response.Content.ReadAsStringAsync().Result; + + /// Parse the results and add the strings to the translated phrases if there was no error, + /// i.e. the target language was returned together with the translated string, which is + /// not the case if the service gives back an error message. + /// + List parsedResults = new JavaScriptSerializer().Deserialize>(result); + if (parsedResults != null) + { + for (int n = 0; n < parsedResults.Count; n++) + { + translatedPhrases.Add((string.IsNullOrEmpty(parsedResults[n].translations[0].to))? "" : parsedResults[n].translations[0].text); + } + } + } + + return translatedPhrases; + } + } +} diff --git a/MetadataTranslator/Metadata Translator/UI/ConnectionStringInput.xaml b/MetadataTranslator/Metadata Translator/UI/ConnectionStringInput.xaml new file mode 100644 index 0000000..e95692c --- /dev/null +++ b/MetadataTranslator/Metadata Translator/UI/ConnectionStringInput.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + diff --git a/MetadataTranslator/Metadata Translator/UI/ImportExportPanel.xaml.cs b/MetadataTranslator/Metadata Translator/UI/ImportExportPanel.xaml.cs new file mode 100644 index 0000000..d15f812 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/UI/ImportExportPanel.xaml.cs @@ -0,0 +1,100 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using WF = System.Windows.Forms; + +namespace Metadata_Translator +{ + /// + /// Interaction logic for ImportExportPanel.xaml + /// + public partial class ImportExportPanel : UserControl + { + public ImportExportPanel() + { + InitializeComponent(); + } + + private void CancelButton_Click(object sender, RoutedEventArgs e) + { + var mainWnd = MainWindow.GetMainWindow(this); + if (mainWnd != null) + { + mainWnd.ImportExportToggle.IsChecked = false; + } + } + + private void OnImportButton_Click(object sender, RoutedEventArgs e) + { + var mainWnd = MainWindow.GetMainWindow(this); + if (mainWnd != null) + { + Button importButton = sender as Button; + + OpenFileDialog openFileDialog1 = new OpenFileDialog(); + + // Set filter options and filter index. + openFileDialog1.Filter = "CSV Files (.csv)|*.csv|All Files (*.*)|*.*"; + openFileDialog1.FilterIndex = 1; + + openFileDialog1.Multiselect = true; + openFileDialog1.CheckFileExists = true; + + if (openFileDialog1.ShowDialog() == true) + { + using (new Hourglass()) + { + + foreach (string filePath in openFileDialog1.FileNames) + { + string lcid = Path.GetFileNameWithoutExtension(filePath); + mainWnd.AddColumn(lcid); + + try + { + mainWnd.DataModel?.ImportFromCsv(filePath, lcid, mainWnd.OverwriteTranslation); + } + catch { } + } + } + } + + mainWnd.ImportExportToggle.IsChecked = false; + } + } + + private void OnExportButton_Click(object sender, RoutedEventArgs e) + { + var mainWnd = MainWindow.GetMainWindow(this); + if (mainWnd != null) + { + Button exportButton = sender as Button; + WF.FolderBrowserDialog folderDlg = new WF.FolderBrowserDialog + { + SelectedPath = mainWnd.LastUsedExportFolder, + Description = (string)exportButton.Tag, + ShowNewFolderButton = true, + }; + + WF.DialogResult result = folderDlg.ShowDialog(); + if (result == WF.DialogResult.OK) + { + mainWnd.LastUsedExportFolder = folderDlg.SelectedPath; + + using (new Hourglass()) + { + mainWnd.DataModel?.ExportToCsv(folderDlg.SelectedPath); + } + } + + mainWnd.ImportExportToggle.IsChecked = false; + } + } + } +} diff --git a/MetadataTranslator/Metadata Translator/UI/LanguagePanel.xaml b/MetadataTranslator/Metadata Translator/UI/LanguagePanel.xaml new file mode 100644 index 0000000..6eda33b --- /dev/null +++ b/MetadataTranslator/Metadata Translator/UI/LanguagePanel.xaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MetadataTranslator/Metadata Translator/UI/LanguagePanel.xaml.cs b/MetadataTranslator/Metadata Translator/UI/LanguagePanel.xaml.cs new file mode 100644 index 0000000..893be04 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/UI/LanguagePanel.xaml.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Metadata_Translator +{ + /// + /// Interaction logic for LanguagePanel.xaml + /// + public partial class LanguagePanel : UserControl + { + public LanguagePanel() + { + InitializeComponent(); + } + + private void CancelButton_Click(object sender, RoutedEventArgs e) + { + var mainWnd = MainWindow.GetMainWindow(this); + if (mainWnd != null) + { + mainWnd.LanguageToggle.IsChecked = false; + } + } + + #region Dependency Properties + public static readonly DependencyProperty LanguagesProperty = + DependencyProperty.Register("Languages", typeof(ObservableCollection), typeof(LanguagePanel), + new PropertyMetadata(null, new PropertyChangedCallback(OnLanguagesChanged))); + + public ObservableCollection Languages + { + get { return (ObservableCollection)GetValue(LanguagesProperty); } + set { SetValue(LanguagesProperty, value); } + } + + private static void OnLanguagesChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if (sender is LanguagePanel langPanel) + { + langPanel.OnLanguagesChanged(e); + } + } + + private void OnLanguagesChanged(DependencyPropertyChangedEventArgs e) + { + if (e.NewValue is ObservableCollection newItem) + { + newItem.CollectionChanged += LanguagesCollection_Changed; + } + else if (e.OldValue is ObservableCollection oldItem) + { + oldItem.CollectionChanged -= LanguagesCollection_Changed; + } + } + + private void LanguagesCollection_Changed(object sender, NotifyCollectionChangedEventArgs e) + { + TranslationGroups = Languages.Select(x => new TranslationGroup { Name = x.TranslationGroup, Tag = x.TranslationId }).Distinct().ToList(); + } + + + public static readonly DependencyProperty TranslationGroupsProperty = + DependencyProperty.Register("TranslationGroups", typeof(List), typeof(LanguagePanel)); + + public List TranslationGroups + { + get { return (List)GetValue(TranslationGroupsProperty); } + set { SetValue(TranslationGroupsProperty, value); } + } + #endregion + } +} diff --git a/MetadataTranslator/Metadata Translator/UI/MainWindow.xaml b/MetadataTranslator/Metadata Translator/UI/MainWindow.xaml new file mode 100644 index 0000000..cde2f1e --- /dev/null +++ b/MetadataTranslator/Metadata Translator/UI/MainWindow.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MetadataTranslator/Metadata Translator/UI/MainWindow.xaml.cs b/MetadataTranslator/Metadata Translator/UI/MainWindow.xaml.cs new file mode 100644 index 0000000..3bbcaa1 --- /dev/null +++ b/MetadataTranslator/Metadata Translator/UI/MainWindow.xaml.cs @@ -0,0 +1,634 @@ +using Microsoft.AnalysisServices.Tabular; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Configuration; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Web.Script.Serialization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Metadata_Translator +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + + SetDependencyProperty("SubscriptionKey"); + SetDependencyProperty("TranslatorEndpoint"); + SetDependencyProperty("TranslatorLocation"); + SetDependencyProperty("OverwriteTranslation"); + SetDependencyProperty("LastUsedExportFolder"); + + if (string.IsNullOrEmpty(SubscriptionKey)) + { + SettingsToggle.IsChecked = true; + } + } + + public MainWindow(StartupEventArgs e) : this() + { + if (e.Args.Length == 2) + { + ConnectToDataset(e.Args[0], e.Args[1]); + } + } + + public void AddColumn(string lcid) + { + Language language = DataModel.GetLanguageByLcid(lcid); + if (language != null) + { + dataGrid.Columns.Add(new DataGridTextColumn + { + Header = language.DisplayName, + Binding = new Binding(lcid) + }); + + language.IsSelected = true; + } + } + + /// + /// Connects to the dataset using a connection string. + /// + /// + public void ConnectToDataset(string connectionString) + { + try + { + DataModel = DataModel.Connect(connectionString); + InitTranslationUI(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message); + } + } + + /// + /// Connects to the dataset via server and database name. + /// + /// + /// + public void ConnectToDataset(string server, string database) + { + try + { + DataModel = DataModel.Connect(server, database); + InitTranslationUI(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message); + Application.Current.Shutdown(); + } + } + + #region Helper functions + /// + /// Initializes the translation grid. + /// + private void InitTranslationUI() + { + PowerBIEngine = DataModel.ServerName; + DatabaseName = DataModel.DatabaseName; + + SetDependencyProperty("Languages"); + InitializeDataGrid(); + } + + /// + /// Set Dependency Properties that wrap apps ettings. + /// + /// + private void SetDependencyProperty(string parameterName) + { + switch (parameterName) + { + case "SubscriptionKey": + SubscriptionKey = Properties.Settings.Default.SubscriptionKey; + break; + case "TranslatorEndpoint": + TranslatorEndpoint = Properties.Settings.Default.TranslatorEndpoint; + break; + case "TranslatorLocation": + TranslatorLocation = Properties.Settings.Default.TranslatorLocation; + break; + case "OverwriteTranslation": + OverwriteTranslation = Properties.Settings.Default.OverwriteTranslation; + break; + case "LastUsedExportFolder": + LastUsedExportFolder = Properties.Settings.Default.LastUsedExportFolder; + break; + case "Languages": + Languages = new ObservableCollection(); + foreach (var language in DataModel.SupportedLanguages) + { + Languages.Add(language); + } + break; + } + } + + /// + /// Initializes the datagrid columns and sets the ItemsSource to the default collection. + /// + private void InitializeDataGrid() + { + List cultures = DataModel.CultureNames; + + /// Create some setters and triggers for the + /// styles of the read-only columns. + /// + Trigger iIsSelectedTrigger = new Trigger() + { + Property = DataGridTextColumn.IsReadOnlyProperty, + Value = true + }; + + var foregroundSetter = new Setter(DataGridCell.ForegroundProperty, new SolidColorBrush(Colors.Black)); + iIsSelectedTrigger.Setters.Add(foregroundSetter); + + var backgroundSetter = new Setter(DataGridCell.BackgroundProperty, new SolidColorBrush(Colors.LightGray)); + + /// The first column is for the metadata container object. + /// + var objectColumnStyle = new Style(typeof(DataGridCell)); + objectColumnStyle.Setters.Add(backgroundSetter); + objectColumnStyle.Setters.Add(foregroundSetter); + objectColumnStyle.Triggers.Add(iIsSelectedTrigger); + dataGrid.Columns.Add(new DataGridTextColumn + { + Header = FindResource("TabularObjectColumnHeader").ToString(), + Binding = new Binding(DataModel.ContainerColumnHeader), + IsReadOnly = true, + CellStyle = objectColumnStyle + }); + + /// The second column is for the default culture of the data model, + /// which is always the first language in the list of data model cultures. + /// + var defaultLangColumnStyle = new Style(typeof(DataGridCell)); + defaultLangColumnStyle.Setters.Add(foregroundSetter); + defaultLangColumnStyle.Triggers.Add(iIsSelectedTrigger); + + /// Add a tooltip to flag that the default culture is readonly. + /// + var objectHeaderStyle = new Style(typeof(DataGridColumnHeader)); + objectHeaderStyle.Setters.Add(new Setter(ToolTipService.ToolTipProperty, + FindResource("DefaultCultureColumnHeaderToolTip").ToString())); + + Language defaultLang = DataModel.GetLanguageByLcid(cultures[0]); + dataGrid.Columns.Add(new DataGridTextColumn + { + Header = $"{defaultLang.DisplayName}*", + HeaderStyle = objectHeaderStyle, + Binding = new Binding(cultures[0]), + IsReadOnly = true, + CellStyle = defaultLangColumnStyle + }); + DataModel.SetLanguageFlags(cultures[0], true, true); + + + /// Add the remaining languages that already exist in the data model + /// and mark them as selected in the list of supported languages. + /// + for (int i = 1; i< cultures.Count; i++) + { + AddColumn(cultures[i]); + } + + /// And set Captions as the default content of the datagrid. + dataGrid.ItemsSource = DataModel.Captions; + } + + /// + /// Get a handle to the main window object so that other user controls can + /// access the public properties of the main window object directly. + /// + /// + /// + public static MainWindow GetMainWindow(DependencyObject child) + { + if (child == null) return null; + + DependencyObject parentObject = VisualTreeHelper.GetParent(child); + if (parentObject is MainWindow parent) + { + return parent; + } + else + { + return GetMainWindow(parentObject); + } + } + #endregion + + #region Dependency Properties + /// + /// DataModel object + /// + public static readonly DependencyProperty DataModelProperty = + DependencyProperty.Register("DataModel", typeof(DataModel), typeof(MainWindow)); + + public DataModel DataModel + { + get { return (DataModel)GetValue(DataModelProperty); } + set { SetValue(DataModelProperty, value); } + } + + /// + /// Supported languages collection + /// + public static readonly DependencyProperty LanguagesProperty = + DependencyProperty.Register("Languages", typeof(ObservableCollection), typeof(MainWindow), + new PropertyMetadata(null, new PropertyChangedCallback(OnLanguagesChanged))); + + public ObservableCollection Languages + { + get { return (ObservableCollection)GetValue(LanguagesProperty); } + set { SetValue(LanguagesProperty, value); } + } + + private static void OnLanguagesChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + (sender as MainWindow).OnLanguagesChanged(e); + } + + private void OnLanguagesChanged(DependencyPropertyChangedEventArgs e) + { + if (e.NewValue is ObservableCollection newCollection) + { + newCollection.CollectionChanged += LanguagesCollection_Changed; + + if (newCollection.Count > 0) + AttachLanguagePropertyChangedEventHandler(newCollection); + } + else if (e.OldValue is ObservableCollection oldCollection) + { + oldCollection.CollectionChanged -= LanguagesCollection_Changed; + + if (oldCollection.Count > 0) + RemoveLanguagePropertyChangedEventHandler(oldCollection); + } + } + + private void LanguagesCollection_Changed(object sender, NotifyCollectionChangedEventArgs e) + { + switch(e.Action) + { + case NotifyCollectionChangedAction.Add: + AttachLanguagePropertyChangedEventHandler(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + RemoveLanguagePropertyChangedEventHandler(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + RemoveLanguagePropertyChangedEventHandler(e.OldItems); + AttachLanguagePropertyChangedEventHandler(e.NewItems); + break; + default: + throw new NotSupportedException(); + } + } + + private void AttachLanguagePropertyChangedEventHandler(System.Collections.IList items) + { + foreach (Language language in items) + { + language.PropertyChanged += LanguageProperty_Changed; + } + } + + private void RemoveLanguagePropertyChangedEventHandler(System.Collections.IList items) + { + foreach (Language language in items) + { + language.PropertyChanged -= LanguageProperty_Changed; + } + } + + /// + /// Event handler for LanguageProperty_Changed event to add or remove a lanugage + /// from the datagrid headers. + /// + /// + /// + private void LanguageProperty_Changed(object sender, PropertyChangedEventArgs e) + { + + if(sender is Language language + && e.PropertyName == "IsSelected" + && !language.LanguageTag.Equals(DataModel.DefaultCulture)) + { + var existingColumn = dataGrid.Columns.Where(x => language.DisplayName.Equals(x.Header.ToString())).FirstOrDefault(); + if (language.IsSelected) + { + if (existingColumn == null) + { + dataGrid.Columns.Add(new DataGridTextColumn + { + Header = language.DisplayName, + Binding = new Binding(language.LanguageTag) + }); + } + } + else + { + if (existingColumn != null) + { + dataGrid.Columns.Remove(existingColumn); + } + } + } + } + + /// + /// SubscriptionKey Property + /// + public static readonly DependencyProperty SubscriptionKeyProperty = + DependencyProperty.Register("SubscriptionKey", typeof(string), typeof(MainWindow), + new PropertyMetadata(new PropertyChangedCallback(OnSubscriptionKeyChanged))); + + public string SubscriptionKey + { + get { return (string)GetValue(SubscriptionKeyProperty); } + set { SetValue(SubscriptionKeyProperty, value); } + } + + private static void OnSubscriptionKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Properties.Settings.Default.SubscriptionKey = (string)e.NewValue; + } + + /// + /// TranslatorEndpoint Property + /// + public static readonly DependencyProperty TranslatorEndpointProperty = + DependencyProperty.Register("TranslatorEndpoint", typeof(string), typeof(MainWindow), + new PropertyMetadata(new PropertyChangedCallback(OnTranslatorEndpointChanged))); + + public string TranslatorEndpoint + { + get { return (string)GetValue(TranslatorEndpointProperty); } + set { SetValue(TranslatorEndpointProperty, value); } + } + + private static void OnTranslatorEndpointChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Properties.Settings.Default.TranslatorEndpoint = (string)e.NewValue; + } + + /// + /// TranslatorLocation Property + /// + public static readonly DependencyProperty TranslatorLocationProperty = + DependencyProperty.Register("TranslatorLocation", typeof(string), typeof(MainWindow), + new PropertyMetadata(new PropertyChangedCallback(OnTranslatorLocationChanged))); + + public string TranslatorLocation + { + get { return (string)GetValue(TranslatorLocationProperty); } + set { SetValue(TranslatorLocationProperty, value); } + } + + private static void OnTranslatorLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Properties.Settings.Default.TranslatorLocation = (string)e.NewValue; + } + + /// + /// OverwriteTranslation Property + /// + public static readonly DependencyProperty OverwriteTranslationProperty = + DependencyProperty.Register("OverwriteTranslation", typeof(bool), typeof(MainWindow), + new PropertyMetadata(new PropertyChangedCallback(OnOverwriteTranslationChanged))); + + public bool OverwriteTranslation + { + get { return (bool)GetValue(OverwriteTranslationProperty); } + set { SetValue(OverwriteTranslationProperty, value); } + } + + private static void OnOverwriteTranslationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Properties.Settings.Default.OverwriteTranslation = (bool)e.NewValue; + } + + /// + /// LastUsedExportFolder Property + /// + public static readonly DependencyProperty LastUsedExportFolderProperty = + DependencyProperty.Register("LastUsedExportFolder", typeof(string), typeof(MainWindow), + new PropertyMetadata(new PropertyChangedCallback(OnLastUsedExportFolderChanged))); + + public string LastUsedExportFolder + { + get { return (string)GetValue(LastUsedExportFolderProperty); } + set { SetValue(LastUsedExportFolderProperty, value); } + } + + private static void OnLastUsedExportFolderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Properties.Settings.Default.LastUsedExportFolder = (string)e.NewValue; + } + + /// + /// PowerBIEngine Property + /// + public static readonly DependencyProperty PowerBIEngineProperty = + DependencyProperty.Register("PowerBIEngine", typeof(string), typeof(MainWindow)); + public string PowerBIEngine + { + set { SetValue(PowerBIEngineProperty, value); } + get { return (string)GetValue(PowerBIEngineProperty); } + } + + /// + /// DatabaseName Property + /// + public static readonly DependencyProperty DatabaseNameProperty = + DependencyProperty.Register("DatabaseName", typeof(string), typeof(MainWindow)); + public string DatabaseName + { + set { SetValue(DatabaseNameProperty, value); } + get { return (string)GetValue(DatabaseNameProperty); } + } + #endregion + + #region Event Handlers + /// + /// Event handler for ToolBar_Loaded event to remove some of the + /// unnecessary standard UI elements of the toolbar. + /// + /// + /// + /// + private void ToolBar_Loaded(object sender, RoutedEventArgs e) + { + if(sender is ToolBar toolBar) + { + if (toolBar.Template.FindName("OverflowGrid", toolBar) is FrameworkElement overflowGrid) + { + overflowGrid.Visibility = Visibility.Collapsed; + } + + if (toolBar.Template.FindName("MainPanelBorder", toolBar) is FrameworkElement mainPanelBorder) + { + mainPanelBorder.Margin = new Thickness(); + } + } + } + + /// + /// Enabling the deselect of a selected toggle button on mouseclick. + /// + /// + /// + private void OnToggleButton_Click(object sender, RoutedEventArgs e) + { + if(sender is RadioButton radio) + { + bool tagState = (radio.Tag == null); + radio.Tag = (radio.Tag == null) ? new object() : null; + radio.IsChecked = tagState; + } + } + + /// + /// Unsets the tag that indicates if a toggle button was clicked to be deselected. + /// + /// + /// + private void OnToggleButton_Uncheck(object sender, RoutedEventArgs e) + { + if (sender is RadioButton radio) + { + radio.Tag = null; + } + } + + /// + /// Display the Captions in the datagrid. + /// + /// + /// + private void OnCaptionRadioButton_Click(object sender, RoutedEventArgs e) + { + if (DataModel != null) + { + dataGrid.ItemsSource = DataModel.Captions; + } + } + + /// + /// Display the Descriptions in the datagrid. + /// + /// + /// + private void OnDescriptionRadioButton_Click(object sender, RoutedEventArgs e) + { + if (DataModel != null) + { + dataGrid.ItemsSource = DataModel.Descriptions; + } + } + + /// + /// Display the DisplayFolder strings in the datagrid. + /// + /// + /// + private void OnDisplayFolderRadioButton_Click(object sender, RoutedEventArgs e) + { + if (DataModel != null) + { + dataGrid.ItemsSource = DataModel.DisplayFolders; + } + } + + /// + /// Make the terms in the default culture more translatable by splitting the strings + /// based on camel casing. + /// + /// + /// + private void OnPrepareButton_Click(object sender, RoutedEventArgs e) + { + if (DataModel == null) return; + + string defaultCulture = DataModel.DefaultCulture; + foreach(ExpandoObject row in DataModel.GetAllDataRows()) + { + ((IDictionary)row)[defaultCulture] = row.SeparateCamelCase(defaultCulture); + } + } + + /// + /// Translate the strings tn the default culture into the + /// selected target languages. + /// + /// + /// + private void OnTranslateButton_Click(object sender, RoutedEventArgs e) + { + if (DataModel.HasTargetLanguages) + { + using (new Hourglass()) + { + var ts = new TranslatorService(Languages.Where(x => x.IsSelected == true)?.ToList(), DataModel.DefaultCulture, SubscriptionKey, TranslatorEndpoint, TranslatorLocation); + ts.Translate(DataModel.GetAllDataRows(), OverwriteTranslation); + } + } + else + { + MessageBox.Show(FindResource("NothingToTranslate").ToString()); + LanguageToggle.IsChecked = true; + } + } + + /// + /// Apply the current values in the Captions, Descriptions, and DisplayFolders collections to the data model. + /// + /// + /// + private void OnApplyButton_Click(object sender, RoutedEventArgs e) + { + DataModel.Update(); + } + + /// + /// Saves modified user settings. + /// + /// + /// + private void OnMainWindow_Closing(object sender, CancelEventArgs e) + { + Properties.Settings.Default.Save(); + } + #endregion + } +} diff --git a/MetadataTranslator/Metadata Translator/UI/SettingsPanel.xaml b/MetadataTranslator/Metadata Translator/UI/SettingsPanel.xaml new file mode 100644 index 0000000..0b5fd7c --- /dev/null +++ b/MetadataTranslator/Metadata Translator/UI/SettingsPanel.xaml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + +