This commit is contained in:
Christian Wade 2021-04-30 19:52:37 -07:00
commit fdc2c8b363
78 changed files with 9719 additions and 1 deletions

View File

@ -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
}
]

View File

@ -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);
}

View File

@ -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.

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{0B0EA3E1-79CC-4674-A763-9C40925DEE31}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>MTCmd</RootNamespace>
<AssemblyName>MTCmd</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.CommandLine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\System.CommandLine.2.0.0-beta1.20574.7\lib\netstandard2.0\System.CommandLine.dll</HintPath>
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Strings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Strings.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Metadata Translator\Metadata Translator.csproj">
<Project>{cb7d493c-b67e-4438-b304-efe5d418addf}</Project>
<Name>Metadata Translator</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Strings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Content Include="icon.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -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<string>(
new string[]{ "--connection-string", "-cs" }, Strings.csDescription){ IsRequired = true },
new Option<Mode>(
new string[]{ "--mode", "-m" },
getDefaultValue: () => Mode.Export,
description: Strings.modeDescription),
new Option<DirectoryInfo>(
new string[]{ "--export-folder", "-ef" }, Strings.efDescription).ExistingOnly(),
new Option<FileInfo>(
new string[]{ "--import-file", "-if" }, Strings.ifDescription).ExistingOnly(),
new Option<string>(
new string[]{ "--locale-id", "-lcid" }, Strings.lcidDescription),
new Option<bool>(
new string[]{ "--fallback-mode", "-fm" }, Strings.fallbackDescription),
new Option<string>(
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<string, Mode, DirectoryInfo, FileInfo, string, bool, string>((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<string, bool> 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);
}
}
}
}

View File

@ -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")]

View File

@ -0,0 +1,225 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MTCmd {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// 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.
/// </summary>
internal static string csDescription {
get {
return ResourceManager.GetString("csDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The full path to the export folder..
/// </summary>
internal static string efDescription {
get {
return ResourceManager.GetString("efDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Existing translations exported to: {0}.
/// </summary>
internal static string exportSuccess {
get {
return ResourceManager.GetString("exportSuccess", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
internal static string fallbackDescription {
get {
return ResourceManager.GetString("fallbackDescription", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
internal static string ifDescription {
get {
return ResourceManager.GetString("ifDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Translation file {0} imported successfully..
/// </summary>
internal static string importSuccess {
get {
return ResourceManager.GetString("importSuccess", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
internal static string invalidFileType {
get {
return ResourceManager.GetString("invalidFileType", resourceCulture);
}
}
/// <summary>
/// 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 &lt;lcid&gt;.csv or &lt;lcid&gt;.resx..
/// </summary>
internal static string invalidLocale {
get {
return ResourceManager.GetString("invalidLocale", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
internal static string keyPrefixDescription {
get {
return ResourceManager.GetString("keyPrefixDescription", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
internal static string lcidDescription {
get {
return ResourceManager.GetString("lcidDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The mode of the command-line operation.
/// </summary>
internal static string modeDescription {
get {
return ResourceManager.GetString("modeDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This dataset has no translations..
/// </summary>
internal static string noExportableTranslations {
get {
return ResourceManager.GetString("noExportableTranslations", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use the --export-folder option to specify an export folder for this operation..
/// </summary>
internal static string noExportFolderSpecified {
get {
return ResourceManager.GetString("noExportFolderSpecified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use the --import-file option to specify an import file for this operation..
/// </summary>
internal static string noImportFileSpecified {
get {
return ResourceManager.GetString("noImportFileSpecified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No translated strings found! The keys of the reference resx &apos;{0}&apos; and the translation resx &apos;{1}&apos; don&apos;t seem to match..
/// </summary>
internal static string NoResxMatches {
get {
return ResourceManager.GetString("NoResxMatches", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unable to import &apos;{0}&apos; 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}.
/// </summary>
internal static string noResxReferenceFile {
get {
return ResourceManager.GetString("noResxReferenceFile", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Metadata Translator command-line app.
/// </summary>
internal static string rootCmdDescription {
get {
return ResourceManager.GetString("rootCmdDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Translations for &apos;{0}&apos; exported to: {1}.
/// </summary>
internal static string singleLocalExportSuccess {
get {
return ResourceManager.GetString("singleLocalExportSuccess", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="csDescription" xml:space="preserve">
<value>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</value>
</data>
<data name="efDescription" xml:space="preserve">
<value>The full path to the export folder.</value>
</data>
<data name="exportSuccess" xml:space="preserve">
<value>Existing translations exported to: {0}</value>
</data>
<data name="fallbackDescription" xml:space="preserve">
<value>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.</value>
</data>
<data name="ifDescription" xml:space="preserve">
<value>The full path and name of the comma-separated values (csv) or resource (resx) translation file to import.</value>
</data>
<data name="importSuccess" xml:space="preserve">
<value>Translation file {0} imported successfully.</value>
</data>
<data name="invalidFileType" xml:space="preserve">
<value>Invalid translation file type. The file must be either a comma-separated values (csv) file or a resource (resx) file.</value>
</data>
<data name="invalidLocale" xml:space="preserve">
<value>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 &lt;lcid&gt;.csv or &lt;lcid&gt;.resx.</value>
</data>
<data name="keyPrefixDescription" xml:space="preserve">
<value>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.</value>
</data>
<data name="lcidDescription" xml:space="preserve">
<value>A valid Windows Language Code Identifier (LCID), aka language tag, such as en-US, af-NA, and zh-CN.</value>
</data>
<data name="modeDescription" xml:space="preserve">
<value>The mode of the command-line operation</value>
</data>
<data name="noExportableTranslations" xml:space="preserve">
<value>This dataset has no translations.</value>
</data>
<data name="noExportFolderSpecified" xml:space="preserve">
<value>Use the --export-folder option to specify an export folder for this operation.</value>
</data>
<data name="noImportFileSpecified" xml:space="preserve">
<value>Use the --import-file option to specify an import file for this operation.</value>
</data>
<data name="NoResxMatches" xml:space="preserve">
<value>No translated strings found! The keys of the reference resx '{0}' and the translation resx '{1}' don't seem to match.</value>
</data>
<data name="noResxReferenceFile" xml:space="preserve">
<value>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}</value>
</data>
<data name="rootCmdDescription" xml:space="preserve">
<value>Metadata Translator command-line app</value>
</data>
<data name="singleLocalExportSuccess" xml:space="preserve">
<value>Translations for '{0}' exported to: {1}</value>
</data>
</root>

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.CSharp" version="4.4.1" targetFramework="net472" />
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
<package id="System.CommandLine" version="2.0.0-beta1.20574.7" targetFramework="net472" />
<package id="System.Memory" version="4.5.4" targetFramework="net472" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
<package id="System.Runtime.CompilerServices.Unsafe" version="4.5.3" targetFramework="net472" />
</packages>

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="Metadata_Translator.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<userSettings>
<Metadata_Translator.Properties.Settings>
<setting name="SubscriptionKey" serializeAs="String">
<value />
</setting>
<setting name="TranslatorEndpoint" serializeAs="String">
<value>https://api.cognitive.microsofttranslator.com/</value>
</setting>
<setting name="TranslatorLocation" serializeAs="String">
<value />
</setting>
<setting name="OverwriteTranslation" serializeAs="String">
<value>False</value>
</setting>
<setting name="LastUsedExportFolder" serializeAs="String">
<value />
</setting>
</Metadata_Translator.Properties.Settings>
</userSettings>
</configuration>

View File

@ -0,0 +1,13 @@
<Application x:Class="Metadata_Translator.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Metadata_Translator"
Startup="Application_Startup">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources\StringDictionary.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -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
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
MainWindow mainWindow = new MainWindow(e);
mainWindow.Show();
}
}
}

View File

@ -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<ExpandoObject> collection) ? collection.Count > 0 : false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<ExpandoObject> 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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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<ExpandoObject> Captions { get; private set; }
public ObservableCollection<ExpandoObject> Descriptions { get; private set; }
public ObservableCollection<ExpandoObject> DisplayFolders { get; private set; }
public string DefaultCulture { get; set; }
public string ServerName { get; private set; }
public string DatabaseName { get; private set; }
public List<string> CultureNames
{
get
{
List<string> cultures = new List<string> { Model.Culture };
cultures.AddRange(Model?.Cultures.Where(i => !i.Name.Equals(Model.Culture)).Select(x => x.Name).ToList());
return cultures;
}
}
public List<Language> SupportedLanguages { get; private set; }
public List<Language> SelectedLanguages { get => SupportedLanguages?.Where(x => x.IsSelected==true).ToList(); }
public bool HasTargetLanguages { get => SelectedLanguages?.Count > 1; }
private const string appTag = "__MT";
/// <summary>
/// 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.
/// </summary>
/// <param name="server"></param>
/// <param name="database"></param>
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="connectionString"></param>
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();
}
/// <summary>
/// Initializes the list of supported languages and the named object collections.
/// </summary>
private void Initialize()
{
LoadLanguages();
DefaultCulture = Model.Culture;
LoadNamedObjectCollections();
}
/// <summary>
/// A static helper to get the DataModel object based on server and database name.
/// </summary>
/// <param name="server"></param>
/// <param name="database"></param>
/// <returns></returns>
public static DataModel Connect(string server, string database)
{
return new DataModel(server, database);
}
/// <summary>
/// A static helper to get the DataModel object based on a connection string.
/// </summary>
/// <param name="connectionString"></param>
/// <returns></returns>
public static DataModel Connect(string connectionString)
{
return new DataModel(connectionString);
}
/// <summary>
/// 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.
/// </summary>
private void LoadNamedObjectCollections()
{
Captions = new ObservableCollection<ExpandoObject>();
Descriptions = new ObservableCollection<ExpandoObject>();
DisplayFolders = new ObservableCollection<ExpandoObject>();
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);
}
}
}
/// <summary>
/// Loads the list of supported languages from the supportedlanguages.json file.
/// </summary>
private void LoadLanguages()
{
SupportedLanguages = new List<Language>();
string content = File.ReadAllText($"{System.AppDomain.CurrentDomain.BaseDirectory}Resources\\supportedlanguages.json");
foreach (Language lang in new JavaScriptSerializer().Deserialize<List<Language>>(content))
{
SupportedLanguages.Add(lang);
}
}
/// <summary>
/// Marks the languages in the list of supported lanuages as selected and model default.
/// </summary>
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);
}
}
}
/// <summary>
/// Marks all translation languages as unselected.
/// </summary>
public void DeselectAllLanguages()
{
foreach(Language lang in SelectedLanguages)
{
if (lang.IsNotModelDefault)
lang.IsSelected = false;
}
}
/// <summary>
/// Marks a language specified by lcid as selected and as model default.
/// </summary>
/// <param name="lcid"></param>
/// <param name="isSelected"></param>
/// <param name="isModelDefault"></param>
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;
}
}
/// <summary>
/// Gets the Language object based on the lcid (i.e. LanguageTag).
/// </summary>
/// <param name="lcid"></param>
/// <returns></returns>
public Language GetLanguageByLcid(string lcid)
{
return this.SupportedLanguages.Where(x => x.LanguageTag.Equals(lcid)).FirstOrDefault();
}
/// <summary>
/// Creates a new ExpandoObject for a source string (displayString).
/// </summary>
/// <param name="objectContainer"></param>
/// <param name="displayString"></param>
/// <param name="defaultCulture"></param>
/// <param name="cultures"></param>
/// <returns>An ExpandoObject representing a data row.</returns>
public ExpandoObject CreateRow(MetadataObjectContainer objectContainer, string displayString, string defaultCulture, CultureCollection cultures)
{
dynamic row = new ExpandoObject();
((IDictionary<String, Object>)row)[ContainerColumnHeader] = objectContainer;
foreach (var culture in cultures)
{
((IDictionary<String, Object>)row)[culture.Name] = culture.Name.Equals(defaultCulture) ? displayString :
culture.ObjectTranslations[objectContainer.TabularObject, objectContainer.TranslatedProperty]?.Value;
}
return row;
}
/// <summary>
/// Combine all collections for translation and updating.
/// </summary>
/// <returns></returns>
public List<ExpandoObject> GetAllDataRows()
{
var allRows = new List<ExpandoObject>();
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;
}
/// <summary>
/// Adds a translation to a Tabular metadata object.
/// </summary>
/// <param name="culture"></param>
/// <param name="metadataObjectContainer"></param>
/// <param name="translation"></param>
private void SetTranslation(Culture culture, MetadataObjectContainer metadataObjectContainer, string translation)
{
culture.ObjectTranslations.SetTranslation(
metadataObjectContainer.TabularObject, metadataObjectContainer.TranslatedProperty,
translation);
}
/// <summary>
/// Updates the Power BI dataset with the translations from the ExpandoObject collections and saves the changes.
/// </summary>
public void Update()
{
/// Delete any deselected cultures that still exist in the dataset.
///
List<string> 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<string, Object>)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);
}
/// <summary>
/// Exports the translations to individual language (csv) files.
/// The files are placed into the specified export folder.
/// </summary>
/// <param name="exportFolderPath"></param>
public void ExportToCsv(string exportFolderPath)
{
string separator = ",";
List<ExpandoObject> dataRows = GetAllDataRows();
if (dataRows != null && dataRows.Count > 0)
{
List<string> 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);
}
}
}
}
}
/// <summary>
/// Exports the translations to individual resx files.
/// The files are placed into the specified export folder.
/// </summary>
/// <param name="exportFolderPath"></param>
public void ExportToResx(string exportFolderPath, string keyPrefix)
{
List<ExpandoObject> dataRows = GetAllDataRows();
if (dataRows != null && dataRows.Count > 0)
{
List<string> 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);
}
}
}
}
}
}
/// <summary>
/// Imports translations from a csv file. The file name must match the LCID of the target language.
/// </summary>
/// <param name="filePath"></param>
/// <param name="lcid"></param>
/// <param name="replaceExistingTranslations"></param>
public void ImportFromCsv(string filePath, string lcid, bool replaceExistingTranslations, bool fallbackToDefaultLocale = false)
{
string csvData = File.ReadAllText(filePath);
if (string.IsNullOrEmpty(csvData)) return;
List<CsvRow> parsedRows = new List<CsvRow>();
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<CsvRow> parsedRows = new List<CsvRow>();
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);
}
/// <summary>
/// Applies a list of translations to the ExpandoObject collections
/// </summary>
/// <param name="lcid"></param>
/// <param name="translatedRows"></param>
/// <param name="replaceExistingTranslations"></param>
private void ApplyTranslation(string lcid, List<CsvRow> 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);
}
}
}
}
/// <summary>
/// Iterates over the dataRows and applies the translated strings with the assumption that
/// translatedRows matches the dataRows in number and order.
/// </summary>
private bool MatchAllRows(List<ExpandoObject> dataRows, string lcid, List<CsvRow> 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;
}
}
}

View File

@ -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<NamedMetadataObject> TabularObjects { get; private set; }
public DisplayFolderContainer(NamedMetadataObject metadataObject, TranslatedProperty translatedProperty) : base(metadataObject, translatedProperty)
{
TabularObjects = new List<NamedMetadataObject>();
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;
}
}
}

View File

@ -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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@ -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.
<img src="https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/Installing%20Metadata%20Translator.png" alt="Installing Metadata Translator" style="zoom:50%;" />
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.
<img src="https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/Launching%20Metadata%20Translator.png" alt="Launching Metadata Translator" style="zoom:38%;" />
## 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.
<img src="https://github.com/microsoft/Analysis-Services/blob/master/MetadataTranslator/Metadata%20Translator/Documentation/Images/Settings.png" alt="Settings" style="zoom:38%;" />
> **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 havent 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 havent 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 dont 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: \<key prefix\>#\<metadata object name\>#\<translated property type\>.
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. <br />Supported values are: <br /> Caption<br /> Description<br /> DisplayFolder<br />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).

View File

@ -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<String, Object>)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<String, Object>)expando).ContainsKey(columnName))
{
return ((IDictionary<String, Object>)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<String, Object>)expando)[columnName] = value;
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<CsvRow> GetValues(this List<ExpandoObject> collection, string containerColumnName, string referenceColumnName, string columnName)
{
if (collection == null) return new List<CsvRow>();
var values = new List<CsvRow>();
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<string, string> GetKeyValuePairs(this List<ExpandoObject> collection, string containerColumnName, string columnName, string namePrefix)
{
if (collection == null) return new Dictionary<string, string>();
var values = new Dictionary<string, string>();
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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<ExpandoObject> collection, int index, string columnName)
{
if (collection == null) return string.Empty;
ExpandoObject row = collection[index];
return ((IDictionary<String, Object>)row)[columnName]?.ToString();
}
public static void SetValueAt(this ObservableCollection<ExpandoObject> collection, int index, string columnName, string value)
{
if (collection == null) return;
ExpandoObject row = collection[index];
((IDictionary<String, Object>)row)[columnName] = value;
}
public static void UpdateDataValues(this ObservableCollection<ExpandoObject> collection, List<string> sourcePhrases, string sourceLanguage, List<string> 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<string> GetValues(this ObservableCollection<ExpandoObject> collection, string columnName)
{
if (collection == null) return new List<string>();
List<string> values = new List<string>();
foreach (ExpandoObject row in collection)
{
string value = ((IDictionary<String, Object>)row)[columnName]?.ToString();
if (!string.IsNullOrEmpty(value))
values.Add(value);
}
return values;
}
public static void AddDisplayFolder(this ObservableCollection<ExpandoObject> collection, NamedMetadataObject metadataObject, string displayString, string defaultCulture, CultureCollection cultures)
{
if (collection == null) return;
if (!string.IsNullOrEmpty(displayString))
{
foreach (ExpandoObject item in collection)
{
if (((IDictionary<String, Object>)item)[defaultCulture] is string displayName && displayName.Equals(displayString))
{
var existingDisplayFolderContainer = ((IDictionary<String, Object>)item)["Object"] as DisplayFolderContainer;
existingDisplayFolderContainer.TabularObjects.Add(metadataObject);
return;
}
}
}
dynamic row = new ExpandoObject();
var displayFolderContainer = new DisplayFolderContainer(metadataObject, TranslatedProperty.DisplayFolder);
((IDictionary<String, Object>)row)["Object"] = displayFolderContainer;
foreach (var culture in cultures)
{
((IDictionary<String, Object>)row)[culture.Name] = culture.Name.Equals(defaultCulture) ? displayString :
culture.ObjectTranslations[displayFolderContainer.TabularObject, displayFolderContainer.TranslatedProperty]?.Value;
}
collection.Add(row);
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,37 @@

namespace Metadata_Translator
{
partial class Installer1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
}
#endregion
}
}

View File

@ -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("<METADATA_TRANSLATOR_PATH>", 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);
}
}
}
}

View File

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CB7D493C-B67E-4438-B304-EFE5D418ADDF}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>Metadata_Translator</RootNamespace>
<AssemblyName>Metadata Translator</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>MetadataTranslator.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AnalysisServices, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AnalysisServices.AdomdClient, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AnalysisServices.AdomdClient.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.AdomdClient.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AnalysisServices.Core, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.Core.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AnalysisServices.SPClient.Interfaces, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.SPClient.Interfaces.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AnalysisServices.Tabular, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.Tabular.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AnalysisServices.Tabular.Json, Version=19.16.3.4, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AnalysisServices.retail.amd64.19.16.3.4\lib\net45\Microsoft.AnalysisServices.Tabular.Json.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualBasic" />
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Data" />
<Reference Include="System.Management" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="Helpers\NoResxMatchesException.cs" />
<Compile Include="UI\ConnectionStringInput.xaml.cs">
<DependentUpon>ConnectionStringInput.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\CollectionEmptyTrueFalseConverter.cs" />
<Compile Include="Converters\TranslationPropertyToolTipConverter.cs" />
<Compile Include="Data\CsvRow.cs" />
<Compile Include="Data\DisplayFolderContainer.cs" />
<Compile Include="Helpers\ExpandoObjectExtensions.cs" />
<Compile Include="Helpers\StringExtensions.cs" />
<Compile Include="Helpers\Hourglass.cs" />
<Compile Include="Helpers\ListExtensions.cs" />
<Compile Include="Helpers\ObservableCollectionExtensions.cs" />
<Compile Include="Installer1.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Installer1.Designer.cs">
<DependentUpon>Installer1.cs</DependentUpon>
</Compile>
<Compile Include="UI\ImportExportPanel.xaml.cs">
<DependentUpon>ImportExportPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Translations\Language.cs" />
<Compile Include="UI\LanguagePanel.xaml.cs">
<DependentUpon>LanguagePanel.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\PercentageConverter.cs" />
<Compile Include="UI\SettingsPanel.xaml.cs">
<DependentUpon>SettingsPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Translations\TranslationGroup.cs" />
<Compile Include="UI\TranslationGroupPanel.xaml.cs">
<DependentUpon>TranslationGroupPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Translations\TranslatorService.cs" />
<Compile Include="Converters\TrueFalseVisibilityConverter.cs" />
<Compile Include="Translations\Translation.cs" />
<Compile Include="Translations\TranslationResult.cs" />
<Page Include="UI\ConnectionStringInput.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\ImportExportPanel.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\LanguagePanel.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="Data\DataModel.cs" />
<Compile Include="UI\MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="UI\SettingsPanel.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Resources\StringDictionary.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\TranslationGroupPanel.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Data\MetadataObjectContainer.cs" />
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="metadata-translator.pbitool.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="MetadataTranslator.snk" />
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<None Include="Resources\supportedlanguages.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Resource Include="icon.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.7.2">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.7.2 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>
</PostBuildEvent>
</PropertyGroup>
</Project>

View File

@ -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
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> 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")]

View File

@ -0,0 +1,70 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Metadata_Translator.Properties
{
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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()
{
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,86 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
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;
}
}
}
}

View File

@ -0,0 +1,21 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="Metadata_Translator.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="SubscriptionKey" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="TranslatorEndpoint" Type="System.String" Scope="User">
<Value Profile="(Default)">https://api.cognitive.microsofttranslator.com/</Value>
</Setting>
<Setting Name="TranslatorLocation" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="OverwriteTranslation" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="LastUsedExportFolder" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
</Settings>
</SettingsFile>

View File

@ -0,0 +1,44 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Metadata_Translator"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="Languages">Languages</system:String>
<system:String x:Key="LanguagesToolTip">Opens the Languages pane to choose the target languages.</system:String>
<system:String x:Key="Settings">Settings</system:String>
<system:String x:Key="SettingsToolTip">Opens the Settings pane to configure Machine Translator.</system:String>
<system:String x:Key="TranslatedProperty">Translated Property</system:String>
<system:String x:Key="Caption">Caption</system:String>
<system:String x:Key="CaptionToolTip">Displays the captions of the metadata objects and their translations.</system:String>
<system:String x:Key="Description">Description</system:String>
<system:String x:Key="DescriptionToolTip">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.</system:String>
<system:String x:Key="DisplayFolder">DisplayFolder</system:String>
<system:String x:Key="DisplayFolderToolTip">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.</system:String>
<system:String x:Key="Prepare">Prepare</system:String>
<system:String x:Key="Translate">Translate</system:String>
<system:String x:Key="TranslateToolTip">Translates the captions, descriptions, and display folder names using Microsoft Translator.</system:String>
<system:String x:Key="Apply">Apply</system:String>
<system:String x:Key="ApplyToolTip">Applies the translated captions, descriptions, and display folder names to the Power BI dataset.</system:String>
<system:String x:Key="ImportExport">Import/Export</system:String>
<system:String x:Key="ImportExportToolTip">Opens the Import/Export pane to export and import the captions, descriptions, and display folder names via .csv files.</system:String>
<system:String x:Key="InvalidArguments">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%\"".</system:String>
<system:String x:Key="DatasetConnection">Dataset Connection String</system:String>
<system:String x:Key="DatasetConnectionPrompt">Enter the connection string to the dataset.</system:String>
<system:String x:Key="SubscriptionKey">Translator Subscription Key</system:String>
<system:String x:Key="TranslatorEndpoint">Translator Endpoint</system:String>
<system:String x:Key="TranslatorLocation">Translator Location</system:String>
<system:String x:Key="OverwriteTranslation">Overwrite Translation</system:String>
<system:String x:Key="SubscriptionKeyPrompt">Enter your Microsoft Translator service subscription key.</system:String>
<system:String x:Key="TranslatorEndpointPrompt">Enter the Microsoft Translator URL, specifically: https://api.cognitive.microsofttranslator.com/</system:String>
<system:String x:Key="TranslatorLocationPrompt">This is typically the Global region, but could also be a single Azure region.</system:String>
<system:String x:Key="NothingToTranslate">Please select at least one language before you translate the metadata.</system:String>
<system:String x:Key="UnableToTranslate">Microsoft Translator was unable to translate the metadata strings into {0}. Please try again later or remove this language from the translation.</system:String>
<system:String x:Key="ExportHeading">Export .csv files</system:String>
<system:String x:Key="ExportDescription">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.</system:String>
<system:String x:Key="ExportFolderDescription">Select the folder where you want to store your .csv files. Note that Metadata Translator might overwrite any existing files in this folder.</system:String>
<system:String x:Key="ExportButtonCaption">Export...</system:String>
<system:String x:Key="ImportHeading">Import .csv files</system:String>
<system:String x:Key="ImportDescription">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.</system:String>
<system:String x:Key="ImportButtonCaption">Import...</system:String>
<system:String x:Key="TabularObjectColumnHeader">Object - Type</system:String>
<system:String x:Key="DefaultCultureColumnHeaderToolTip">The default culture is read-only. Use Power BI Desktop to change the names, descriptions, and display folder names for the default culture.</system:String>
</ResourceDictionary>

File diff suppressed because it is too large Load Diff

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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();
}
}
}

View File

@ -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<Translation> translations { get; set; }
}
}

View File

@ -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<Language> Languages { get; set; }
string SourceLanguage { get; set; }
string SubscriptionKey { get; set; }
string Endpoint { get; set; }
string Location { get; set; }
/// <summary>
///
/// </summary>
/// <param name="sourceLanguage"></param>
/// <param name="targetLanguage"></param>
/// <param name="subscriptionKey"></param>
/// <param name="endpoint"></param>
/// <param name="location"></param>
public TranslatorService(List<Language> languages, string sourceLanguage, string subscriptionKey, string endpoint, string location)
{
Languages = languages;
SourceLanguage = sourceLanguage;
SubscriptionKey = subscriptionKey;
Endpoint = endpoint;
Location = location;
}
public void Translate(List<ExpandoObject> 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<Language> targetLanguages = Languages.Where(l => !l.LanguageTag.Equals(SourceLanguage)).ToList();
/// Filter down the data rows to those with values in the source language.
///
List<ExpandoObject> 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<ExpandoObject> dataRows, IEnumerable<Language> targetLanguages, string translationId, bool replaceExistingTranslations)
{
List<ExpandoObject> rowsToTranslate = new List<ExpandoObject>();
/// 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<ExpandoObject> dataRows, IEnumerable<Language> 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<object> translationBatch = new List<object>();
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<string> TranslateBatch(List<object> sourceObjects, string targetLanguage)
{
List<string> translatedPhrases = new List<string>();
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<TranslationResult> parsedResults = new JavaScriptSerializer().Deserialize<List<TranslationResult>>(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;
}
}
}

View File

@ -0,0 +1,51 @@
<UserControl x:Class="Metadata_Translator.ConnectionStringInput"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Metadata_Translator"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="{StaticResource DatasetConnection}" Margin="4,0"/>
<TextBox x:Name="ConnectionString" Grid.Row="0" Grid.Column="1" VerticalContentAlignment="Center">
<TextBox.Style>
<Style TargetType="TextBox" xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Style.Resources>
<VisualBrush x:Key="CueBrush" AlignmentX="Left" AlignmentY="Center" Stretch="None">
<VisualBrush.Visual>
<Grid>
<Rectangle Width="3000" Height="100" Fill="White"/>
<Label Content="{StaticResource DatasetConnectionPrompt}" Foreground="Gray" FontStyle="Italic" VerticalAlignment="Center"/>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Style.Resources>
<Style.Triggers>
<Trigger Property="Text" Value="{x:Static sys:String.Empty}">
<Setter Property="Background" Value="{StaticResource CueBrush}" />
</Trigger>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Background" Value="{StaticResource CueBrush}" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="Background" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<StackPanel Grid.Column="1" Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="OK" Margin="4" Width="48" Click="OKButton_Click"/>
<Button Content="Cancel" Margin="4" Width="48" Click="CancelButton_Click"/>
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
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
{
/// <summary>
/// Interaction logic for ConnectionStringInput.xaml
/// </summary>
public partial class ConnectionStringInput : UserControl
{
public ConnectionStringInput()
{
InitializeComponent();
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
var mainWnd = MainWindow.GetMainWindow(this);
if (mainWnd != null)
{
mainWnd.ConnectToDataset(ConnectionString.Text);
}
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}
}
}

View File

@ -0,0 +1,90 @@
<UserControl x:Class="Metadata_Translator.ImportExportPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Metadata_Translator"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<local:PercentageConverter x:Key="PercentageConverter" />
</UserControl.Resources>
<Grid x:Name="underline" MinWidth="200" Margin="8,2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Height="16" Width="16" Background="Transparent" Margin="0,2,4,2"
BorderThickness="0" HorizontalAlignment="Right" VerticalAlignment="Top"
Click="CancelButton_Click">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Transform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</DrawingGroup.Transform>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="1"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color="Gray"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Button>
<StackPanel Grid.Row="1" Margin="4,0">
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" DockPanel.Dock="Top" Text="{StaticResource ExportHeading}" FontStyle="Italic" FontWeight="DemiBold"/>
<Rectangle Grid.Row="1" DockPanel.Dock="Top" Fill="Gray" Height="0.5"
Width="{Binding ElementName=underline, Path=ActualWidth, Converter={StaticResource PercentageConverter}}"
HorizontalAlignment="Left"/>
<TextBlock Grid.Row="2" Text="{StaticResource ExportDescription}" TextWrapping="Wrap" Width="260"
HorizontalAlignment="Left"/>
<Button Grid.Row="3" Margin="8,0" Padding="4" BorderThickness="0" Background="Transparent" Cursor="Hand"
Click="OnExportButton_Click" Tag="{StaticResource ExportFolderDescription}"
HorizontalAlignment="Right" >
<TextBlock TextDecorations="Underline" Text="{StaticResource ExportButtonCaption}" FontStyle="Italic" Foreground="Blue"/>
</Button>
</Grid>
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" DockPanel.Dock="Top" Text="{StaticResource ImportHeading}" FontStyle="Italic" FontWeight="DemiBold"/>
<Rectangle Grid.Row="1" DockPanel.Dock="Top" Fill="Gray" Height="0.5"
HorizontalAlignment="Left"
Width="{Binding ElementName=underline, Path=ActualWidth, Converter={StaticResource PercentageConverter}}" />
<TextBlock Grid.Row="2" Text="{StaticResource ImportDescription}" TextWrapping="Wrap" Width="260"
HorizontalAlignment="Left"/>
<Button Grid.Row="3" Click="OnImportButton_Click" HorizontalAlignment="Right" Margin="8,0" Padding="4" BorderThickness="0" Background="Transparent" Cursor="Hand">
<TextBlock TextDecorations="Underline" Text="{StaticResource ImportButtonCaption}" FontStyle="Italic" Foreground="Blue"/>
</Button>
</Grid>
</StackPanel>
</Grid>
</UserControl>

View File

@ -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
{
/// <summary>
/// Interaction logic for ImportExportPanel.xaml
/// </summary>
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;
}
}
}
}

View File

@ -0,0 +1,89 @@
<UserControl x:Class="Metadata_Translator.LanguagePanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Metadata_Translator"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
mc:Ignorable="d"
x:Name="lPanel"
d:DesignHeight="450" d:DesignWidth="800" Background="#FFF9F5EB">
<UserControl.Resources>
<CollectionViewSource x:Key="TranslationGroups"
Source="{Binding TranslationGroups, ElementName=lPanel}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Name"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<local:PercentageConverter x:Key="PercentageConverter" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Column="1" Height="16" Width="16" Background="Transparent" Margin="0,2,18,2"
BorderThickness="0" HorizontalAlignment="Right" VerticalAlignment="Top"
Click="CancelButton_Click">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Transform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</DrawingGroup.Transform>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="1"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color="Gray"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Button>
<ScrollViewer Grid.Row="1" x:Name="underline">
<ItemsControl ItemsSource="{Binding Source={StaticResource TranslationGroups}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Margin="4,0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" DockPanel.Dock="Top" Text="{Binding Name}" FontStyle="Italic" FontWeight="DemiBold" Margin="4,0"/>
<Rectangle Grid.Row="1" DockPanel.Dock="Top" Fill="Gray" Height="0.5" Width="{Binding ElementName=underline, Path=ActualWidth, Converter={StaticResource PercentageConverter}}" />
<local:TranslationGroupPanel Grid.Row="2" Grid.ColumnSpan="2" Margin="18,0,4,4"
Languages="{Binding Languages, ElementName=lPanel}"
TranslationGroupId="{Binding Tag}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -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
{
/// <summary>
/// Interaction logic for LanguagePanel.xaml
/// </summary>
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<Language>), typeof(LanguagePanel),
new PropertyMetadata(null, new PropertyChangedCallback(OnLanguagesChanged)));
public ObservableCollection<Language> Languages
{
get { return (ObservableCollection<Language>)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<Language> newItem)
{
newItem.CollectionChanged += LanguagesCollection_Changed;
}
else if (e.OldValue is ObservableCollection<Language> 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<TranslationGroup>), typeof(LanguagePanel));
public List<TranslationGroup> TranslationGroups
{
get { return (List<TranslationGroup>)GetValue(TranslationGroupsProperty); }
set { SetValue(TranslationGroupsProperty, value); }
}
#endregion
}
}

View File

@ -0,0 +1,302 @@
<Window x:Class="Metadata_Translator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Metadata_Translator"
mc:Ignorable="d"
x:Name="main"
Title="Metadata Translator" Height="450" Width="800"
Closing="OnMainWindow_Closing">
<Window.Resources>
<local:TrueFalseVisibilityConverter x:Key="TrueFalseVisibilityConverter" />
<local:FalseTrueVisibilityConverter x:Key="FalseTrueVisibilityConverter" />
<local:CollectionEmptyTrueFalseConverter x:Key="CollectionEmptyTrueFalseConverter" />
<local:TranslationPropertyToolTipConverter x:Key="TranslationPropertyToolTipConverter" />
</Window.Resources>
<Grid Margin="0">
<local:ConnectionStringInput VerticalAlignment="Center" Margin="16" Visibility="{Binding DataModel, ElementName=main, Converter={StaticResource FalseTrueVisibilityConverter}}"/>
<Grid Margin="0" Visibility="{Binding DataModel, ElementName=main, Converter={StaticResource TrueFalseVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ToolBarPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="0">
<ToolBar ToolBarTray.IsLocked="True" Loaded="ToolBar_Loaded">
<RadioButton x:Name="LanguageToggle" GroupName="PanelButtons" Height="48" Width="48"
ToolTip="{StaticResource LanguagesToolTip}"
Click="OnToggleButton_Click" Unchecked="OnToggleButton_Uncheck">
<StackPanel>
<Image Height="24" Width="32">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Transform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</DrawingGroup.Transform>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M152.1 236.2c-3.5-12.1-7.8-33.2-7.8-33.2h-.5s-4.3 21.1-7.8 33.2l-11.1 37.5H163zM616 96H336v320h280c13.3 0 24-10.7 24-24V120c0-13.3-10.7-24-24-24zm-24 120c0 6.6-5.4 12-12 12h-11.4c-6.9 23.6-21.7 47.4-42.7 69.9 8.4 6.4 17.1 12.5 26.1 18 5.5 3.4 7.3 10.5 4.1 16.2l-7.9 13.9c-3.4 5.9-10.9 7.8-16.7 4.3-12.6-7.8-24.5-16.1-35.4-24.9-10.9 8.7-22.7 17.1-35.4 24.9-5.8 3.5-13.3 1.6-16.7-4.3l-7.9-13.9c-3.2-5.6-1.4-12.8 4.2-16.2 9.3-5.7 18-11.7 26.1-18-7.9-8.4-14.9-17-21-25.7-4-5.7-2.2-13.6 3.7-17.1l6.5-3.9 7.3-4.3c5.4-3.2 12.4-1.7 16 3.4 5 7 10.8 14 17.4 20.9 13.5-14.2 23.8-28.9 30-43.2H412c-6.6 0-12-5.4-12-12v-16c0-6.6 5.4-12 12-12h64v-16c0-6.6 5.4-12 12-12h16c6.6 0 12 5.4 12 12v16h64c6.6 0 12 5.4 12 12zM0 120v272c0 13.3 10.7 24 24 24h280V96H24c-13.3 0-24 10.7-24 24zm58.9 216.1L116.4 167c1.7-4.9 6.2-8.1 11.4-8.1h32.5c5.1 0 9.7 3.3 11.4 8.1l57.5 169.1c2.6 7.8-3.1 15.9-11.4 15.9h-22.9a12 12 0 0 1-11.5-8.6l-9.4-31.9h-60.2l-9.1 31.8c-1.5 5.1-6.2 8.7-11.5 8.7H70.3c-8.2 0-14-8.1-11.4-15.9z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="10"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color="#FF2F7C03"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock FontSize="9" HorizontalAlignment="Center" Text="{StaticResource Languages}"/>
</StackPanel>
</RadioButton>
<RadioButton x:Name="SettingsToggle" GroupName="PanelButtons" Height="48" Width="48"
ToolTip="{StaticResource SettingsToolTip}"
Click="OnToggleButton_Click" Unchecked="OnToggleButton_Uncheck">
<StackPanel>
<Image Height="24">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Transform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</DrawingGroup.Transform>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M113.595,133.642l-5.932-13.169c5.655-4.151,10.512-9.315,14.307-15.209l13.507,5.118c2.583,0.979,5.469-0.322,6.447-2.904
l4.964-13.103c0.47-1.24,0.428-2.616-0.117-3.825c-0.545-1.209-1.547-2.152-2.788-2.622l-13.507-5.118
c1.064-6.93,0.848-14.014-0.637-20.871l13.169-5.932c1.209-0.545,2.152-1.547,2.622-2.788c0.47-1.24,0.428-2.616-0.117-3.825
l-5.755-12.775c-1.134-2.518-4.096-3.638-6.612-2.505l-13.169,5.932c-4.151-5.655-9.315-10.512-15.209-14.307l5.118-13.507
c0.978-2.582-0.322-5.469-2.904-6.447L93.88,0.82c-1.239-0.469-2.615-0.428-3.825,0.117c-1.209,0.545-2.152,1.547-2.622,2.788
l-5.117,13.506c-6.937-1.07-14.033-0.849-20.872,0.636L55.513,4.699c-0.545-1.209-1.547-2.152-2.788-2.622
c-1.239-0.469-2.616-0.428-3.825,0.117L36.124,7.949c-2.518,1.134-3.639,4.094-2.505,6.612l5.932,13.169
c-5.655,4.151-10.512,9.315-14.307,15.209l-13.507-5.118c-1.239-0.469-2.615-0.427-3.825,0.117
c-1.209,0.545-2.152,1.547-2.622,2.788L0.326,53.828c-0.978,2.582,0.322,5.469,2.904,6.447l13.507,5.118
c-1.064,6.929-0.848,14.015,0.637,20.871L4.204,92.196c-1.209,0.545-2.152,1.547-2.622,2.788c-0.47,1.24-0.428,2.616,0.117,3.825
l5.755,12.775c0.544,1.209,1.547,2.152,2.787,2.622c1.241,0.47,2.616,0.429,3.825-0.117l13.169-5.932
c4.151,5.656,9.314,10.512,15.209,14.307l-5.118,13.507c-0.978,2.582,0.322,5.469,2.904,6.447l13.103,4.964
c0.571,0.216,1.172,0.324,1.771,0.324c0.701,0,1.402-0.147,2.054-0.441c1.209-0.545,2.152-1.547,2.622-2.788l5.117-13.506
c6.937,1.069,14.034,0.849,20.872-0.636l5.931,13.168c0.545,1.209,1.547,2.152,2.788,2.622c1.24,0.47,2.617,0.429,3.825-0.117
l12.775-5.754C113.607,139.12,114.729,136.16,113.595,133.642z M105.309,86.113c-4.963,13.1-17.706,21.901-31.709,21.901
c-4.096,0-8.135-0.744-12.005-2.21c-8.468-3.208-15.18-9.522-18.899-17.779c-3.719-8.256-4-17.467-0.792-25.935
c4.963-13.1,17.706-21.901,31.709-21.901c4.096,0,8.135,0.744,12.005,2.21c8.468,3.208,15.18,9.522,18.899,17.778
C108.237,68.434,108.518,77.645,105.309,86.113z M216.478,154.389c-0.896-0.977-2.145-1.558-3.469-1.615l-9.418-0.404
c-0.867-4.445-2.433-8.736-4.633-12.697l6.945-6.374c2.035-1.867,2.17-5.03,0.303-7.064l-6.896-7.514
c-0.896-0.977-2.145-1.558-3.47-1.615c-1.322-0.049-2.618,0.416-3.595,1.312l-6.944,6.374c-3.759-2.531-7.9-4.458-12.254-5.702
l0.404-9.418c0.118-2.759-2.023-5.091-4.782-5.209l-10.189-0.437c-2.745-0.104-5.091,2.023-5.209,4.781l-0.404,9.418
c-4.444,0.867-8.735,2.433-12.697,4.632l-6.374-6.945c-0.896-0.977-2.145-1.558-3.469-1.615c-1.324-0.054-2.618,0.416-3.595,1.312
l-7.514,6.896c-2.035,1.867-2.17,5.03-0.303,7.064l6.374,6.945c-2.531,3.759-4.458,7.899-5.702,12.254l-9.417-0.404
c-2.747-0.111-5.092,2.022-5.21,4.781l-0.437,10.189c-0.057,1.325,0.415,2.618,1.312,3.595c0.896,0.977,2.145,1.558,3.47,1.615
l9.417,0.403c0.867,4.445,2.433,8.736,4.632,12.698l-6.944,6.374c-0.977,0.896-1.558,2.145-1.615,3.469
c-0.057,1.325,0.415,2.618,1.312,3.595l6.896,7.514c0.896,0.977,2.145,1.558,3.47,1.615c1.319,0.053,2.618-0.416,3.595-1.312
l6.944-6.374c3.759,2.531,7.9,4.458,12.254,5.702l-0.404,9.418c-0.118,2.759,2.022,5.091,4.781,5.209l10.189,0.437
c0.072,0.003,0.143,0.004,0.214,0.004c1.25,0,2.457-0.468,3.381-1.316c0.977-0.896,1.558-2.145,1.615-3.469l0.404-9.418
c4.444-0.867,8.735-2.433,12.697-4.632l6.374,6.945c0.896,0.977,2.145,1.558,3.469,1.615c1.33,0.058,2.619-0.416,3.595-1.312
l7.514-6.896c2.035-1.867,2.17-5.03,0.303-7.064l-6.374-6.945c2.531-3.759,4.458-7.899,5.702-12.254l9.417,0.404
c2.756,0.106,5.091-2.022,5.21-4.781l0.437-10.189C217.847,156.659,217.375,155.366,216.478,154.389z M160.157,183.953
c-12.844-0.55-22.846-11.448-22.295-24.292c0.536-12.514,10.759-22.317,23.273-22.317c0.338,0,0.678,0.007,1.019,0.022
c12.844,0.551,22.846,11.448,22.295,24.292C183.898,174.511,173.106,184.497,160.157,183.953z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="10"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color="#FFDEA210"/>
<GradientStop Color="#DEA210" Offset="0.992"/>
<GradientStop Color="#AADEA210" Offset="0.527"/>
<GradientStop Color="#FFDEA210" Offset="0.644"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock FontSize="9" HorizontalAlignment="Center" Text="{StaticResource Settings}"/>
</StackPanel>
</RadioButton>
<Separator/>
<StackPanel>
<TextBlock FontSize="9" Text="{StaticResource TranslatedProperty}" HorizontalAlignment="Center" Margin="0,4,0,-4"/>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<StackPanel ToolTip="{StaticResource CaptionToolTip}">
<RadioButton GroupName="TranslationProperty" IsChecked="True" HorizontalAlignment="Center" Click="OnCaptionRadioButton_Click"/>
<TextBlock FontSize="8" Text="{StaticResource Caption}" Padding="2,0"/>
</StackPanel>
<StackPanel ToolTip="{Binding DataModel.Descriptions, ElementName=main, Converter={StaticResource TranslationPropertyToolTipConverter}, ConverterParameter={StaticResource DescriptionToolTip}}">
<RadioButton GroupName="TranslationProperty" HorizontalAlignment="Center" Click="OnDescriptionRadioButton_Click"
IsEnabled="{Binding DataModel.Descriptions, ElementName=main, Converter={StaticResource CollectionEmptyTrueFalseConverter}}"/>
<TextBlock FontSize="8" Text="{StaticResource Description}" Padding="2,0"/>
</StackPanel>
<StackPanel ToolTip="{Binding DataModel.DisplayFolders, ElementName=main, Converter={StaticResource TranslationPropertyToolTipConverter}, ConverterParameter={StaticResource DisplayFolderToolTip}}">
<RadioButton GroupName="TranslationProperty" HorizontalAlignment="Center" Click="OnDisplayFolderRadioButton_Click"
IsEnabled="{Binding DataModel.DisplayFolders, ElementName=main, Converter={StaticResource CollectionEmptyTrueFalseConverter}}"/>
<TextBlock FontSize="8" Text="{StaticResource DisplayFolder}" Padding="2,0"/>
</StackPanel>
</StackPanel>
</StackPanel>
<Separator/>
<Button Height="48" Width="48" Click="OnPrepareButton_Click" Visibility="Collapsed">
<StackPanel>
<Image Height="24" Width="32">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Transform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</DrawingGroup.Transform>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M104 224H24c-13.255 0-24 10.745-24 24v240c0 13.255 10.745 24 24 24h80c13.255 0 24-10.745 24-24V248c0-13.255-10.745-24-24-24zM64 472c-13.255 0-24-10.745-24-24s10.745-24 24-24 24 10.745 24 24-10.745 24-24 24zM384 81.452c0 42.416-25.97 66.208-33.277 94.548h101.723c33.397 0 59.397 27.746 59.553 58.098.084 17.938-7.546 37.249-19.439 49.197l-.11.11c9.836 23.337 8.237 56.037-9.308 79.469 8.681 25.895-.069 57.704-16.382 74.757 4.298 17.598 2.244 32.575-6.148 44.632C440.202 511.587 389.616 512 346.839 512l-2.845-.001c-48.287-.017-87.806-17.598-119.56-31.725-15.957-7.099-36.821-15.887-52.651-16.178-6.54-.12-11.783-5.457-11.783-11.998v-213.77c0-3.2 1.282-6.271 3.558-8.521 39.614-39.144 56.648-80.587 89.117-113.111 14.804-14.832 20.188-37.236 25.393-58.902C282.515 39.293 291.817 0 312 0c24 0 72 8 72 81.452z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="10"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color="#FFAF800D"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock FontSize="9" HorizontalAlignment="Center" Text="{StaticResource Prepare}"/>
</StackPanel>
</Button>
<Button Height="48" Width="48" Click="OnTranslateButton_Click" ToolTip="{StaticResource TranslateToolTip}">
<StackPanel>
<Image Source="..\icon.ico" Height="24"/>
<TextBlock FontSize="9" HorizontalAlignment="Center" Text="{StaticResource Translate}"/>
</StackPanel>
</Button>
<Button Height="48" Width="48" Click="OnApplyButton_Click" ToolTip="{StaticResource ApplyToolTip}">
<StackPanel>
<Image Height="24" Width="32">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Transform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</DrawingGroup.Transform>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M400 480H48c-26.51 0-48-21.49-48-48V80c0-26.51 21.49-48 48-48h352c26.51 0 48 21.49 48 48v352c0 26.51-21.49 48-48 48zm-204.686-98.059l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.248-16.379-6.249-22.628 0L184 302.745l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.25 16.379 6.25 22.628.001z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="10"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color="#FF2F7C03"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock FontSize="9" HorizontalAlignment="Center" Text="{StaticResource Apply}"/>
</StackPanel>
</Button>
</ToolBar>
</ToolBarPanel>
<ToolBarPanel Grid.Row="0" Grid.Column="2" Margin="0">
<ToolBar ToolBarTray.IsLocked="True" Loaded="ToolBar_Loaded">
<RadioButton x:Name="ImportExportToggle" GroupName="PanelButtons" Height="48" Width="68"
ToolTip="{StaticResource ImportExportToolTip}"
Click="OnToggleButton_Click" Unchecked="OnToggleButton_Uncheck">
<StackPanel>
<Image Height="24">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Transform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</DrawingGroup.Transform>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M224 136V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zm-96 144c0 4.42-3.58 8-8 8h-8c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h8c4.42 0 8 3.58 8 8v16c0 4.42-3.58 8-8 8h-8c-26.51 0-48-21.49-48-48v-32c0-26.51 21.49-48 48-48h8c4.42 0 8 3.58 8 8v16zm44.27 104H160c-4.42 0-8-3.58-8-8v-16c0-4.42 3.58-8 8-8h12.27c5.95 0 10.41-3.5 10.41-6.62 0-1.3-.75-2.66-2.12-3.84l-21.89-18.77c-8.47-7.22-13.33-17.48-13.33-28.14 0-21.3 19.02-38.62 42.41-38.62H200c4.42 0 8 3.58 8 8v16c0 4.42-3.58 8-8 8h-12.27c-5.95 0-10.41 3.5-10.41 6.62 0 1.3.75 2.66 2.12 3.84l21.89 18.77c8.47 7.22 13.33 17.48 13.33 28.14.01 21.29-19 38.62-42.39 38.62zM256 264v20.8c0 20.27 5.7 40.17 16 56.88 10.3-16.7 16-36.61 16-56.88V264c0-4.42 3.58-8 8-8h16c4.42 0 8 3.58 8 8v20.8c0 35.48-12.88 68.89-36.28 94.09-3.02 3.25-7.27 5.11-11.72 5.11s-8.7-1.86-11.72-5.11c-23.4-25.2-36.28-58.61-36.28-94.09V264c0-4.42 3.58-8 8-8h16c4.42 0 8 3.58 8 8zm121-159L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128v-6.1c0-6.3-2.5-12.4-7-16.9z">
<GeometryDrawing.Pen>
<Pen Brush="White" Thickness="8"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color="Black"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock FontSize="9" HorizontalAlignment="Center" Text="{StaticResource ImportExport}"/>
</StackPanel>
</RadioButton>
</ToolBar>
</ToolBarPanel>
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<local:SettingsPanel Grid.Row="0" Grid.ColumnSpan="3"
Visibility="{Binding IsChecked, ElementName=SettingsToggle, Converter={StaticResource FalseTrueVisibilityConverter}}"/>
<local:LanguagePanel Grid.Row="1" Visibility="{Binding IsChecked, ElementName=LanguageToggle, Converter={StaticResource FalseTrueVisibilityConverter}}"
Languages="{Binding Languages, ElementName=main}"/>
<ScrollViewer Grid.Row="1" Grid.Column="1" HorizontalScrollBarVisibility="Auto">
<DataGrid x:Name="dataGrid" AutoGenerateColumns="False" CanUserAddRows="False" BorderThickness="0"/>
</ScrollViewer>
<local:ImportExportPanel Grid.Row="1" Grid.Column="2" Visibility="{Binding IsChecked, ElementName=ImportExportToggle, Converter={StaticResource FalseTrueVisibilityConverter}}"/>
</Grid>
</Grid>
</Grid>
</Window>

View File

@ -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
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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;
}
}
/// <summary>
/// Connects to the dataset using a connection string.
/// </summary>
/// <param name="connectionString"></param>
public void ConnectToDataset(string connectionString)
{
try
{
DataModel = DataModel.Connect(connectionString);
InitTranslationUI();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
/// <summary>
/// Connects to the dataset via server and database name.
/// </summary>
/// <param name="server"></param>
/// <param name="database"></param>
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
/// <summary>
/// Initializes the translation grid.
/// </summary>
private void InitTranslationUI()
{
PowerBIEngine = DataModel.ServerName;
DatabaseName = DataModel.DatabaseName;
SetDependencyProperty("Languages");
InitializeDataGrid();
}
/// <summary>
/// Set Dependency Properties that wrap apps ettings.
/// </summary>
/// <param name="parameterName"></param>
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<Language>();
foreach (var language in DataModel.SupportedLanguages)
{
Languages.Add(language);
}
break;
}
}
/// <summary>
/// Initializes the datagrid columns and sets the ItemsSource to the default collection.
/// </summary>
private void InitializeDataGrid()
{
List<string> 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;
}
/// <summary>
/// Get a handle to the main window object so that other user controls can
/// access the public properties of the main window object directly.
/// </summary>
/// <param name="child"></param>
/// <returns></returns>
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
/// <summary>
/// DataModel object
/// </summary>
public static readonly DependencyProperty DataModelProperty =
DependencyProperty.Register("DataModel", typeof(DataModel), typeof(MainWindow));
public DataModel DataModel
{
get { return (DataModel)GetValue(DataModelProperty); }
set { SetValue(DataModelProperty, value); }
}
/// <summary>
/// Supported languages collection
/// </summary>
public static readonly DependencyProperty LanguagesProperty =
DependencyProperty.Register("Languages", typeof(ObservableCollection<Language>), typeof(MainWindow),
new PropertyMetadata(null, new PropertyChangedCallback(OnLanguagesChanged)));
public ObservableCollection<Language> Languages
{
get { return (ObservableCollection<Language>)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<Language> newCollection)
{
newCollection.CollectionChanged += LanguagesCollection_Changed;
if (newCollection.Count > 0)
AttachLanguagePropertyChangedEventHandler(newCollection);
}
else if (e.OldValue is ObservableCollection<Language> 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;
}
}
/// <summary>
/// Event handler for LanguageProperty_Changed event to add or remove a lanugage
/// from the datagrid headers.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
}
}
/// <summary>
/// SubscriptionKey Property
/// </summary>
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;
}
/// <summary>
/// TranslatorEndpoint Property
/// </summary>
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;
}
/// <summary>
/// TranslatorLocation Property
/// </summary>
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;
}
/// <summary>
/// OverwriteTranslation Property
/// </summary>
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;
}
/// <summary>
/// LastUsedExportFolder Property
/// </summary>
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;
}
/// <summary>
/// PowerBIEngine Property
/// </summary>
public static readonly DependencyProperty PowerBIEngineProperty =
DependencyProperty.Register("PowerBIEngine", typeof(string), typeof(MainWindow));
public string PowerBIEngine
{
set { SetValue(PowerBIEngineProperty, value); }
get { return (string)GetValue(PowerBIEngineProperty); }
}
/// <summary>
/// DatabaseName Property
/// </summary>
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
/// <summary>
/// Event handler for ToolBar_Loaded event to remove some of the
/// unnecessary standard UI elements of the toolbar.
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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();
}
}
}
/// <summary>
/// Enabling the deselect of a selected toggle button on mouseclick.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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;
}
}
/// <summary>
/// Unsets the tag that indicates if a toggle button was clicked to be deselected.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnToggleButton_Uncheck(object sender, RoutedEventArgs e)
{
if (sender is RadioButton radio)
{
radio.Tag = null;
}
}
/// <summary>
/// Display the Captions in the datagrid.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnCaptionRadioButton_Click(object sender, RoutedEventArgs e)
{
if (DataModel != null)
{
dataGrid.ItemsSource = DataModel.Captions;
}
}
/// <summary>
/// Display the Descriptions in the datagrid.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnDescriptionRadioButton_Click(object sender, RoutedEventArgs e)
{
if (DataModel != null)
{
dataGrid.ItemsSource = DataModel.Descriptions;
}
}
/// <summary>
/// Display the DisplayFolder strings in the datagrid.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnDisplayFolderRadioButton_Click(object sender, RoutedEventArgs e)
{
if (DataModel != null)
{
dataGrid.ItemsSource = DataModel.DisplayFolders;
}
}
/// <summary>
/// Make the terms in the default culture more translatable by splitting the strings
/// based on camel casing.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnPrepareButton_Click(object sender, RoutedEventArgs e)
{
if (DataModel == null) return;
string defaultCulture = DataModel.DefaultCulture;
foreach(ExpandoObject row in DataModel.GetAllDataRows())
{
((IDictionary<String, Object>)row)[defaultCulture] = row.SeparateCamelCase(defaultCulture);
}
}
/// <summary>
/// Translate the strings tn the default culture into the
/// selected target languages.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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;
}
}
/// <summary>
/// Apply the current values in the Captions, Descriptions, and DisplayFolders collections to the data model.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnApplyButton_Click(object sender, RoutedEventArgs e)
{
DataModel.Update();
}
/// <summary>
/// Saves modified user settings.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnMainWindow_Closing(object sender, CancelEventArgs e)
{
Properties.Settings.Default.Save();
}
#endregion
}
}

View File

@ -0,0 +1,117 @@
<UserControl x:Class="Metadata_Translator.SettingsPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Metadata_Translator"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
IsVisibleChanged="Visibility_Changed" Background="#FFE7EBEC">
<Grid x:Name="grid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="{StaticResource DatasetConnection}" Margin="4,10,8,0" VerticalContentAlignment="Center"/>
<Label Grid.Row="1" Content="{StaticResource SubscriptionKey}" Margin="4,0,8,0"/>
<Label Grid.Row="2" Content="{StaticResource TranslatorEndpoint}" Margin="4,0,8,0"/>
<Label Grid.Row="3" Content="{StaticResource TranslatorLocation}" Margin="4,0,8,0"/>
<TextBox x:Name="AsServerInfo" Grid.Column="1" BorderThickness="0" Background="Transparent" IsReadOnly="True" Margin="4,10,8,0" VerticalContentAlignment="Center" Foreground="Gray"/>
<TextBox x:Name="SubscriptionKey" Grid.Row="1" Grid.Column="1" Margin="4,2,26,2">
<TextBox.Style>
<Style TargetType="TextBox" xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Style.Resources>
<VisualBrush x:Key="CueBrush" AlignmentX="Left" AlignmentY="Center" Stretch="None">
<VisualBrush.Visual>
<Grid>
<Rectangle Width="3000" Height="100" Fill="White"/>
<Label Content="{StaticResource SubscriptionKeyPrompt}" Foreground="Gray" FontStyle="Italic" VerticalAlignment="Center"/>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Style.Resources>
<Style.Triggers>
<Trigger Property="Text" Value="{x:Static sys:String.Empty}">
<Setter Property="Background" Value="{StaticResource CueBrush}" />
</Trigger>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Background" Value="{StaticResource CueBrush}" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="Background" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<TextBox x:Name="TranslatorEndpoint" Grid.Row="2" Grid.Column="1" Margin="4,2,26,2">
<TextBox.Style>
<Style TargetType="TextBox" xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Style.Resources>
<VisualBrush x:Key="CueBrush" AlignmentX="Left" AlignmentY="Center" Stretch="None">
<VisualBrush.Visual>
<Grid>
<Rectangle Width="3000" Height="100" Fill="White"/>
<Label Content="{StaticResource TranslatorEndpointPrompt}" Foreground="Gray" FontStyle="Italic" VerticalAlignment="Center"/>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Style.Resources>
<Style.Triggers>
<Trigger Property="Text" Value="{x:Static sys:String.Empty}">
<Setter Property="Background" Value="{StaticResource CueBrush}" />
</Trigger>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Background" Value="{StaticResource CueBrush}" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="Background" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<TextBox x:Name="TranslatorLocation" Grid.Row="3" Grid.Column="1" Margin="4,2,26,2">
<TextBox.Style>
<Style TargetType="TextBox" xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Style.Resources>
<VisualBrush x:Key="CueBrush" AlignmentX="Left" AlignmentY="Center" Stretch="None">
<VisualBrush.Visual>
<Grid>
<Rectangle Width="3000" Height="100" Fill="White"/>
<Label Content="{StaticResource TranslatorLocationPrompt}" Foreground="Gray" FontStyle="Italic" VerticalAlignment="Center"/>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Style.Resources>
<Style.Triggers>
<Trigger Property="Text" Value="{x:Static sys:String.Empty}">
<Setter Property="Background" Value="{StaticResource CueBrush}" />
</Trigger>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Background" Value="{StaticResource CueBrush}" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="Background" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<StackPanel Grid.Column="1" Grid.Row="4" Orientation="Horizontal" Margin="4,0">
<CheckBox x:Name="OverwriteTranslation" VerticalAlignment="Center"/>
<Label Content="{StaticResource OverwriteTranslation}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="OK" Margin="4" Width="48" Click="OKButton_Click"/>
<Button Content="Cancel" Margin="4" Width="48" Click="CancelButton_Click"/>
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
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
{
/// <summary>
/// Interaction logic for SettingsPanel.xaml
/// </summary>
public partial class SettingsPanel : UserControl
{
public SettingsPanel()
{
InitializeComponent();
DataContext = this;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
var mainWnd = MainWindow.GetMainWindow(this);
if(mainWnd != null)
{
mainWnd.SettingsToggle.IsChecked = false;
}
}
private void Visibility_Changed(object sender, DependencyPropertyChangedEventArgs e)
{
var mainWnd = MainWindow.GetMainWindow(this);
if (mainWnd == null) return;
if (e.NewValue is Boolean show && show == true)
{
AsServerInfo.Text = $"Data Source={mainWnd.PowerBIEngine};Initial Catalog={mainWnd.DatabaseName}";
SubscriptionKey.Text = mainWnd.SubscriptionKey;
TranslatorEndpoint.Text = mainWnd.TranslatorEndpoint;
TranslatorLocation.Text = mainWnd.TranslatorLocation;
OverwriteTranslation.IsChecked = mainWnd.OverwriteTranslation;
}
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
var mainWnd = MainWindow.GetMainWindow(this);
if (mainWnd != null)
{
mainWnd.SubscriptionKey = SubscriptionKey.Text;
mainWnd.TranslatorEndpoint = TranslatorEndpoint.Text;
mainWnd.TranslatorLocation = TranslatorLocation.Text;
mainWnd.OverwriteTranslation = (bool) OverwriteTranslation.IsChecked;
mainWnd.SettingsToggle.IsChecked = false;
}
}
}
}

View File

@ -0,0 +1,37 @@
<UserControl x:Class="Metadata_Translator.TranslationGroupPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:local="clr-namespace:Metadata_Translator"
mc:Ignorable="d"
x:Name="tgPanel"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<CollectionViewSource x:Key="FilteredLanguages"
Source="{Binding Languages, ElementName=tgPanel}"
Filter="Languages_Filter">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="DisplayName"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</UserControl.Resources>
<Grid Margin="0,4,0,0">
<ItemsControl ItemsSource="{Binding Source={StaticResource FilteredLanguages}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Margin="1" VerticalAlignment="Center" IsEnabled="{Binding IsNotModelDefault}"/>
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" Margin="2,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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
{
/// <summary>
/// Interaction logic for TranslationGroupPanel.xaml
/// </summary>
public partial class TranslationGroupPanel : UserControl
{
public TranslationGroupPanel()
{
InitializeComponent();
}
#region Dependency Properties
public static readonly DependencyProperty LanguagesProperty =
DependencyProperty.Register("Languages", typeof(ObservableCollection<Language>), typeof(TranslationGroupPanel));
public ObservableCollection<Language> Languages
{
get { return (ObservableCollection<Language>)GetValue(LanguagesProperty); }
set { SetValue(LanguagesProperty, value); }
}
public static readonly DependencyProperty TranslationGroupIdProperty =
DependencyProperty.Register("TranslationGroupId", typeof(string), typeof(TranslationGroupPanel));
public string TranslationGroupId
{
get { return (string)GetValue(TranslationGroupIdProperty); }
set { SetValue(TranslationGroupIdProperty, value); }
}
#endregion
private void Languages_Filter(object sender, FilterEventArgs e)
{
e.Accepted = (e.Item is Language langItem && langItem.TranslationId == TranslationGroupId);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.AnalysisServices.AdomdClient.retail.amd64" version="19.16.3.4" targetFramework="net472" />
<package id="Microsoft.AnalysisServices.retail.amd64" version="19.16.3.4" targetFramework="net472" />
</packages>

View File

@ -0,0 +1,8 @@
# 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)
[Metadata Translator Documentation](Metadata%20Translator/Documentation/README.md)

View File

@ -23,7 +23,13 @@ BISM Normalizer is a schema diff tool for tabular models
Sample U-SQL scripts that demonstrate how to process a TPC-DS data set in Azure Data Lake.
## [ASJobGraph](https://github.com/Microsoft/Analysis-Services/tree/master/ASJobGraphEvents)
Python script to reassemble job graph events from Anlysis Services.
Python script to reassemble job graph events from Analysis Services.
## [BestPracticeRules](https://github.com/microsoft/Analysis-Services/tree/master/BestPracticeRules)
A curated set of rules covering best practices for tabular model performance and design which can be run from [Tabular Editor's](https://tabulareditor.com/ "Tabular Editor") [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer").
## [Metadata Translator](https://github.com/microsoft/Analysis-Services/tree/master/MetadataTranslator)
Metadata Translator can translate the names, descriptions, and display folders of the metadata objects in a semantic model by using Azure Cognitive Services.
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of