]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9193 WS api/l10n/index returns the effective BCP47 language tag used
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Thu, 11 May 2017 13:26:03 +0000 (15:26 +0200)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Fri, 12 May 2017 10:37:19 +0000 (12:37 +0200)
server/sonar-server/src/main/java/org/sonar/server/platform/ws/IndexAction.java
server/sonar-server/src/main/resources/org/sonar/server/platform/ws/l10n-index-example.json
server/sonar-server/src/test/java/org/sonar/server/platform/ws/IndexActionTest.java
sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java
sonar-core/src/test/java/org/sonar/core/i18n/DefaultI18nTest.java

index 5cf60d89d1ce2479e790aec2dd5fbc092100a86f..99cc708ad64626b0870de5d7b63f83587acc8424 100644 (file)
@@ -73,9 +73,11 @@ public class IndexAction implements WsAction {
     Locale locale = Locale.forLanguageTag(localeParam);
     checkArgument(!locale.getISO3Language().isEmpty(), "'%s' cannot be parsed as a BCP47 language tag", localeParam);
     JsonWriter json = response.newJsonWriter().beginObject();
-    for (String messageKey : i18n.getPropertyKeys()) {
-      json.prop(messageKey, i18n.message(locale, messageKey, messageKey));
-    }
+    json.prop("effectiveLocale", i18n.getEffectiveLocale(locale).toLanguageTag());
+    json.name("messages");
+    json.beginObject();
+    i18n.getPropertyKeys().forEach(messageKey -> json.prop(messageKey, i18n.message(locale, messageKey, messageKey)));
+    json.endObject();
     json.endObject().close();
   }
 }
index 96e8d6bac838a473fddf755b51e2b30ea634fc79..3a6f9f27b5f115cb7361b36292b8545ae64aa2db 100644 (file)
 {
-  "cancel": "Cancel",
-  "rule.php.S115.param.format": "Regular expression used to check the constant names against.",
-  "views.projects.selection_mode": "Project selection mode",
-  "rule.php.S1067.param.max": "Maximum number of allowed conditional operators in an expression",
-  "quality_profiles.restore_built_in_profiles": "Restore Built-in Profiles",
-  "qualifiers.delete_confirm.TRK": "Do you want to delete this project?",
-  "roles.page": "Project Permissions",
-  "metric.package_cycles.abbreviation": "Pkgs cycles",
-  "views.delete_success": "View definition \"{0}\" has been successfully deleted",
-  "project_history.col.month": "Month",
-  "including_abbreviated": "incl.",
-  "coding_rules.filters.activation.help": "Activation criterion is available when a quality profile is selected",
-  "metric.it_branch_coverage.description": "Condition coverage by integration tests",
-  "measure_filter.sharing": "Sharing",
-  "widget.issue_filter.property.filter.name": "Filter",
-  "help_tips": "Help tips",
-  "select_a_metric": "Select a metric",
-  "custom_measures.page.description": "Update the values of custom metrics for this project. Changes will take effect at the project's next analysis. Custom metrics must be created at the global level.",
-  "measure_filter.abbr.description": "Description",
-  "action_plans.delete": "Delete Action Plan",
-  "optional": "Optional",
-  "issue.changelog.field.assignee": "Assignee",
-  "background_tasks.table.duration": "Duration",
-  "source_viewer.tooltip.ut.partially-covered": "Partially covered by unit tests. Click for details.",
-  "size": "Size",
-  "action_plans.confirm_delete": "Delete this action plan? Associated issues will not be deleted.",
-  "quality_gates.delete_condition.confirm.message": "Are you sure you want to delete the \"{0}\" condition?",
-  "rule.php.S1808.param.one_space_before": "There should be exactly one space between control structure keyword and opening parenthesis or curly brace",
-  "metric.new_coverage.description": "Coverage of new/changed code",
-  "widget.time_machine.property.metric9.name": "Metric 9",
-  "views.projects.by_regexp": "All projects matching regular expression \"{0}\"",
-  "user.add_scm_account": "Add SCM account",
-  "to.downcase": "to",
-  "widget.sqaleSunburst.cant_display": "SQALE technical debt is 0 so the breakdown by characteristic can't be displayed.",
-  "measure_filter.name_contains": "Name contains",
-  "name_too_long_x": "Name is too long (maximum is {0} characters)",
-  "metric.line_coverage.name": "Line coverage",
-  "measure_filter.criteria.key": "Key",
-  "widget.project_issue_filter.property.filter.name": "Filter",
-  "widget.measure_filter_bubble_chart.name": "Measure Filter as Bubble Chart",
-  "issue.assign.formlink": "Assign",
-  "metric.overall_lines_to_cover.description": "Lines to cover by all tests",
-  "no_results": "No results",
-  "rule.php.S1808.param.one_space_after": "There should be exactly one space between closing parenthesis and opening curly braces",
-  "result": "Result",
-  "measure_filter.key_contains": "Key contains",
-  "widget.complexity.description": "Reports on complexity, average complexity and complexity distribution.",
-  "analysis_reports.past_reports": "Past Reports",
-  "property.sonar.global.exclusions.name": "Global Source File Exclusions",
-  "metric.overall_coverage.description": "Overall test coverage",
-  "quality_profiles.remove_projects_confirm_button": "Remove All",
-  "widget.measure_filter_histogram.name": "Measure Filter as Histogram",
-  "metric.covered_lines.description": "Covered lines",
-  "widget.measure_filter_pie_chart.property.extraMetric2.name": "Extra Metric 2",
-  "coding_rules.most_violated_projects": "Most Violated Projects",
-  "views.edit_subview": "Edit \"{0}\"",
-  "over_x_days": "over {0} days",
-  "project_settings.page": "General Settings",
-  "projects_role.codeviewer.desc": "View the project's source code. (Users will also need \"Browse\" permission)",
-  "issue.resolution.REMOVED": "Removed",
-  "coverage_viewer.overall_tests": "All Tests",
-  "metric.test_failures.description": "Number of unit test failures",
-  "dashboard.error_unshare_default": "This dashboard can't be unshared as long as it's defined as a default dashboard",
-  "issue.effort": "Effort:",
-  "background_tasks.failures": "failures",
-  "property.category.exclusions.duplications.description": "Configure the files that should be ignored by duplication detection.",
-  "widget.treemap-widget.name": "Treemap of Components",
-  "widget.measure_filter_histogram.description": "Displays the result of a pre-configured measure filter as a histogram.",
-  "property.category.codeCoverage": "Code Coverage",
-  "component": "Component",
-  "metric.duplications_data.description": "Duplications details",
-  "widget.time_machine.property.metric2.name": "Metric 2",
-  "quality_gates.operator.GT.short": ">",
-  "rules.not_found": "The rule \"{0}\" does not exist",
-  "coding_rules.create": "Create",
-  "days": "Days",
-  "email_configuration.test.send": "Send Test Email",
-  "update_key.replace": "Replace",
-  "views.invalid.criteria": "Only one criteria can be used (regexp, language or manual measure)",
-  "widget.measure_filter_pie_chart.property.chartHeight.name": "Chart Height",
-  "dashboard.project_not_found": "The requested project does not exist. Either it has never been analyzed successfully or it has been deleted.",
-  "global_permissions.admin": "Administer System",
-  "test_viewer.skipped": "skipped",
-  "analysis_reports.x_reports": "{0} reports",
-  "shortcuts.section.global.shortcuts": "open this window",
-  "metric.total-useless-lines.name": "Total Useless Code",
-  "dashboard.default_dashboard": "This dashboard is the default one and is displayed when clicking on \"Overview\".",
-  "component_navigation.status.in_progress.admin": "The analysis is in progress.<br>More details available on the <a href=\"{0}\">Background Tasks</a> page.",
-  "metric.public_api.name": "Public API",
-  "widget.measure_filter_treemap.property.heightInPercents.name": "Height",
-  "quality_profiles.rename_x_title": "Rename Profile {0} - {1}",
-  "widget.select_project": "Please select a project",
-  "coding_rules.change_severity_in": "Change Severity In",
-  "rule.php.S101.param.format": "Regular expression used to check the class names against.",
-  "select2.tooShort": "Please enter at least {0} characters",
-  "widget.custom_measures.property.metric10.name": "Metric 10",
-  "manual_measures.save_button": "Save",
-  "issue_filter.sharing": "Sharing",
-  "widget.measure_filter_cloud.property.colorMetric.name": "Color Metric",
-  "dashboard.TimeMachine.name": "Time Machine",
-  "property.category.security": "Security",
-  "coding_rules.validation.invalid_rule_key": "The rule key \"%s\" is invalid, it should only contain: a-z, 0-9, \"_\"",
-  "metric.sqale_rating.description": "Density of technical debt computed by dividing the technical debt by the estimated effort to develop from scratch an application.",
-  "notification.channel.EmailNotificationChannel": "Email"
+  "locale": "en",
+  "messages": {
+    "cancel": "Cancel",
+    "rule.php.S115.param.format": "Regular expression used to check the constant names against.",
+    "views.projects.selection_mode": "Project selection mode",
+    "rule.php.S1067.param.max": "Maximum number of allowed conditional operators in an expression",
+    "quality_profiles.restore_built_in_profiles": "Restore Built-in Profiles",
+    "qualifiers.delete_confirm.TRK": "Do you want to delete this project?",
+    "roles.page": "Project Permissions",
+    "metric.package_cycles.abbreviation": "Pkgs cycles",
+    "views.delete_success": "View definition \"{0}\" has been successfully deleted",
+    "project_history.col.month": "Month",
+    "including_abbreviated": "incl.",
+    "coding_rules.filters.activation.help": "Activation criterion is available when a quality profile is selected",
+    "metric.it_branch_coverage.description": "Condition coverage by integration tests",
+    "measure_filter.sharing": "Sharing",
+    "widget.issue_filter.property.filter.name": "Filter",
+    "help_tips": "Help tips",
+    "select_a_metric": "Select a metric",
+    "custom_measures.page.description": "Update the values of custom metrics for this project. Changes will take effect at the project's next analysis. Custom metrics must be created at the global level.",
+    "measure_filter.abbr.description": "Description",
+    "action_plans.delete": "Delete Action Plan",
+    "optional": "Optional",
+    "issue.changelog.field.assignee": "Assignee",
+    "background_tasks.table.duration": "Duration",
+    "source_viewer.tooltip.ut.partially-covered": "Partially covered by unit tests. Click for details.",
+    "size": "Size",
+    "action_plans.confirm_delete": "Delete this action plan? Associated issues will not be deleted.",
+    "quality_gates.delete_condition.confirm.message": "Are you sure you want to delete the \"{0}\" condition?",
+    "rule.php.S1808.param.one_space_before": "There should be exactly one space between control structure keyword and opening parenthesis or curly brace",
+    "metric.new_coverage.description": "Coverage of new/changed code",
+    "widget.time_machine.property.metric9.name": "Metric 9",
+    "views.projects.by_regexp": "All projects matching regular expression \"{0}\"",
+    "user.add_scm_account": "Add SCM account",
+    "to.downcase": "to",
+    "widget.sqaleSunburst.cant_display": "SQALE technical debt is 0 so the breakdown by characteristic can't be displayed.",
+    "measure_filter.name_contains": "Name contains",
+    "name_too_long_x": "Name is too long (maximum is {0} characters)",
+    "metric.line_coverage.name": "Line coverage",
+    "measure_filter.criteria.key": "Key",
+    "widget.project_issue_filter.property.filter.name": "Filter",
+    "widget.measure_filter_bubble_chart.name": "Measure Filter as Bubble Chart",
+    "issue.assign.formlink": "Assign",
+    "metric.overall_lines_to_cover.description": "Lines to cover by all tests",
+    "no_results": "No results",
+    "rule.php.S1808.param.one_space_after": "There should be exactly one space between closing parenthesis and opening curly braces",
+    "result": "Result",
+    "measure_filter.key_contains": "Key contains",
+    "widget.complexity.description": "Reports on complexity, average complexity and complexity distribution.",
+    "analysis_reports.past_reports": "Past Reports",
+    "property.sonar.global.exclusions.name": "Global Source File Exclusions",
+    "metric.overall_coverage.description": "Overall test coverage",
+    "quality_profiles.remove_projects_confirm_button": "Remove All",
+    "widget.measure_filter_histogram.name": "Measure Filter as Histogram",
+    "metric.covered_lines.description": "Covered lines",
+    "widget.measure_filter_pie_chart.property.extraMetric2.name": "Extra Metric 2",
+    "coding_rules.most_violated_projects": "Most Violated Projects",
+    "views.edit_subview": "Edit \"{0}\"",
+    "over_x_days": "over {0} days",
+    "project_settings.page": "General Settings",
+    "projects_role.codeviewer.desc": "View the project's source code. (Users will also need \"Browse\" permission)",
+    "issue.resolution.REMOVED": "Removed",
+    "coverage_viewer.overall_tests": "All Tests",
+    "metric.test_failures.description": "Number of unit test failures",
+    "dashboard.error_unshare_default": "This dashboard can't be unshared as long as it's defined as a default dashboard",
+    "issue.effort": "Effort:",
+    "background_tasks.failures": "failures",
+    "property.category.exclusions.duplications.description": "Configure the files that should be ignored by duplication detection.",
+    "widget.treemap-widget.name": "Treemap of Components",
+    "widget.measure_filter_histogram.description": "Displays the result of a pre-configured measure filter as a histogram.",
+    "property.category.codeCoverage": "Code Coverage",
+    "component": "Component",
+    "metric.duplications_data.description": "Duplications details",
+    "widget.time_machine.property.metric2.name": "Metric 2",
+    "quality_gates.operator.GT.short": ">",
+    "rules.not_found": "The rule \"{0}\" does not exist",
+    "coding_rules.create": "Create",
+    "days": "Days",
+    "email_configuration.test.send": "Send Test Email",
+    "update_key.replace": "Replace",
+    "views.invalid.criteria": "Only one criteria can be used (regexp, language or manual measure)",
+    "widget.measure_filter_pie_chart.property.chartHeight.name": "Chart Height",
+    "dashboard.project_not_found": "The requested project does not exist. Either it has never been analyzed successfully or it has been deleted.",
+    "global_permissions.admin": "Administer System",
+    "test_viewer.skipped": "skipped",
+    "analysis_reports.x_reports": "{0} reports",
+    "shortcuts.section.global.shortcuts": "open this window",
+    "metric.total-useless-lines.name": "Total Useless Code",
+    "dashboard.default_dashboard": "This dashboard is the default one and is displayed when clicking on \"Overview\".",
+    "component_navigation.status.in_progress.admin": "The analysis is in progress.<br>More details available on the <a href=\"{0}\">Background Tasks</a> page.",
+    "metric.public_api.name": "Public API",
+    "widget.measure_filter_treemap.property.heightInPercents.name": "Height",
+    "quality_profiles.rename_x_title": "Rename Profile {0} - {1}",
+    "widget.select_project": "Please select a project",
+    "coding_rules.change_severity_in": "Change Severity In",
+    "rule.php.S101.param.format": "Regular expression used to check the class names against.",
+    "select2.tooShort": "Please enter at least {0} characters",
+    "widget.custom_measures.property.metric10.name": "Metric 10",
+    "manual_measures.save_button": "Save",
+    "issue_filter.sharing": "Sharing",
+    "widget.measure_filter_cloud.property.colorMetric.name": "Color Metric",
+    "dashboard.TimeMachine.name": "Time Machine",
+    "property.category.security": "Security",
+    "coding_rules.validation.invalid_rule_key": "The rule key \"%s\" is invalid, it should only contain: a-z, 0-9, \"_\"",
+    "metric.sqale_rating.description": "Density of technical debt computed by dividing the technical debt by the estimated effort to develop from scratch an application.",
+    "notification.channel.EmailNotificationChannel": "Email"
+  }
 }
index 1b0a4b4f8a2defef848bc52e9e6640558c6f06ad..b4c80950d7e3ae279de8438477e07b648085cc09 100644 (file)
@@ -81,6 +81,7 @@ public class IndexActionTest {
     when(i18n.message(PRC, KEY_1, KEY_1)).thenReturn(KEY_1);
     when(i18n.message(PRC, KEY_2, KEY_2)).thenReturn(KEY_2);
     when(i18n.message(PRC, KEY_3, KEY_3)).thenReturn(KEY_3);
+    when(i18n.getEffectiveLocale(PRC)).thenReturn(PRC);
 
     TestResponse result = call(PRC.toLanguageTag(), DateUtils.formatDateTime(aBitEarlier));
 
@@ -88,7 +89,7 @@ public class IndexActionTest {
     verify(i18n).message(PRC, KEY_1, KEY_1);
     verify(i18n).message(PRC, KEY_2, KEY_2);
     verify(i18n).message(PRC, KEY_3, KEY_3);
-    assertJson(result.getInput()).isSimilarTo("{\"key1\":\"key1\",\"key2\":\"key2\",\"key3\":\"key3\"}");
+    assertJson(result.getInput()).isSimilarTo("{\"effectiveLocale\":\"zh-CN\", \"messages\": {\"key1\":\"key1\",\"key2\":\"key2\",\"key3\":\"key3\"}}");
   }
 
   @Test
@@ -100,6 +101,7 @@ public class IndexActionTest {
     when(i18n.message(ENGLISH, key1, key1)).thenReturn(key1);
     when(i18n.message(ENGLISH, key2, key2)).thenReturn(key2);
     when(i18n.message(ENGLISH, key3, key3)).thenReturn(key3);
+    when(i18n.getEffectiveLocale(ENGLISH)).thenReturn(ENGLISH);
 
     TestResponse result = call(null, null);
 
@@ -107,7 +109,7 @@ public class IndexActionTest {
     verify(i18n).message(ENGLISH, key1, key1);
     verify(i18n).message(ENGLISH, key2, key2);
     verify(i18n).message(ENGLISH, key3, key3);
-    assertJson(result.getInput()).isSimilarTo("{\"key1\":\"key1\",\"key2\":\"key2\",\"key3\":\"key3\"}");
+    assertJson(result.getInput()).isSimilarTo("{\"messages\": {\"key1\":\"key1\",\"key2\":\"key2\",\"key3\":\"key3\"}}");
   }
 
   @Test
@@ -115,12 +117,13 @@ public class IndexActionTest {
     String key1 = "key1";
     when(i18n.getPropertyKeys()).thenReturn(ImmutableSet.of(key1));
     when(i18n.message(UK, key1, key1)).thenReturn(key1);
+    when(i18n.getEffectiveLocale(UK)).thenReturn(UK);
 
     TestResponse result = call("en-GB", null);
 
     verify(i18n).getPropertyKeys();
     verify(i18n).message(UK, key1, key1);
-    assertJson(result.getInput()).isSimilarTo("{\"key1\":\"key1\"}");
+    assertJson(result.getInput()).isSimilarTo("{\"messages\": {\"key1\":\"key1\"}}");
   }
 
   @Test
@@ -128,6 +131,7 @@ public class IndexActionTest {
     String key1 = "key1";
     when(i18n.getPropertyKeys()).thenReturn(ImmutableSet.of(key1));
     when(i18n.message(UK, key1, key1)).thenReturn(key1);
+    when(i18n.getEffectiveLocale(UK)).thenReturn(UK);
 
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("'en_GB' cannot be parsed as a BCP47 language tag");
index 42a1707e1823ad1b057fe0810fc8625b4ca9991f..ae746c6580b652f94ed705504ebb6c9fa62131df 100644 (file)
@@ -226,8 +226,14 @@ public class DefaultI18n implements I18n, Startable {
     return propertyToBundles.keySet();
   }
 
+  public Locale getEffectiveLocale(Locale locale) {
+    Locale bundleLocale = ResourceBundle.getBundle(BUNDLE_PACKAGE + "core", locale, this.classloader, this.control).getLocale();
+    locale.getISO3Language();
+    return bundleLocale.getLanguage().isEmpty() ? Locale.ENGLISH : bundleLocale;
+  }
+
   @CheckForNull
-  private String formatMessage(@Nullable String message, Object... parameters) {
+  private static String formatMessage(@Nullable String message, Object... parameters) {
     if (message == null || parameters.length == 0) {
       return message;
     }
index 409d7d94eaf4b9076ae088084774134f909c1c1e..985810e8cb61578e01c81c428de601d9276f4f2a 100644 (file)
@@ -145,6 +145,7 @@ public class DefaultI18nTest {
     assertThat(underTest.message(Locale.CHINA, "checkstyle.rule1.name", null)).isEqualTo("Rule one");
     assertThat(underTest.message(Locale.CHINA, "any", null)).isEqualTo("Any");
     assertThat(underTest.message(Locale.CHINA, "sqale.page", null)).isEqualTo("Sqale page title");
+    assertThat(underTest.getEffectiveLocale(Locale.CHINA)).isEqualTo(Locale.ENGLISH);
   }
 
   @Test