diff options
author | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2017-05-11 15:26:03 +0200 |
---|---|---|
committer | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2017-05-12 12:37:19 +0200 |
commit | b451f859c4fc8e0a694c18e175ddc931e1702cd9 (patch) | |
tree | 5058793bdd549539bd6de42c905dc70891862b00 | |
parent | 4367c635b9e660bec4e30929143d6b1bfd32f2de (diff) | |
download | sonarqube-b451f859c4fc8e0a694c18e175ddc931e1702cd9.tar.gz sonarqube-b451f859c4fc8e0a694c18e175ddc931e1702cd9.zip |
SONAR-9193 WS api/l10n/index returns the effective BCP47 language tag used
5 files changed, 127 insertions, 111 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/IndexAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/IndexAction.java index 5cf60d89d1c..99cc708ad64 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/IndexAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/IndexAction.java @@ -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(); } } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/l10n-index-example.json b/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/l10n-index-example.json index 96e8d6bac83..3a6f9f27b5f 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/l10n-index-example.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/platform/ws/l10n-index-example.json @@ -1,106 +1,109 @@ { - "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" + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/IndexActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/IndexActionTest.java index 1b0a4b4f8a2..b4c80950d7e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/IndexActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/IndexActionTest.java @@ -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"); diff --git a/sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java b/sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java index 42a1707e182..ae746c6580b 100644 --- a/sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java +++ b/sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java @@ -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; } diff --git a/sonar-core/src/test/java/org/sonar/core/i18n/DefaultI18nTest.java b/sonar-core/src/test/java/org/sonar/core/i18n/DefaultI18nTest.java index 409d7d94eaf..985810e8cb6 100644 --- a/sonar-core/src/test/java/org/sonar/core/i18n/DefaultI18nTest.java +++ b/sonar-core/src/test/java/org/sonar/core/i18n/DefaultI18nTest.java @@ -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 |