From: Fabrice Bellingard Date: Fri, 22 Jun 2012 12:04:44 +0000 (+0200) Subject: SONAR-3581 Tool to validate a l10n bundle based on multiple plugins X-Git-Tag: 3.2~295 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=bf998970a385d499990284f6db90ab1247d90029;p=sonarqube.git SONAR-3581 Tool to validate a l10n bundle based on multiple plugins --- diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java index 7ed8b3384a2..4ae41847827 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java @@ -19,6 +19,7 @@ */ package org.sonar.test.i18n; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.io.IOUtils; @@ -26,8 +27,16 @@ import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.sonar.test.TestUtils; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.util.Collection; import java.util.Map; @@ -42,22 +51,32 @@ import static org.junit.Assert.fail; public class BundleSynchronizedMatcher extends BaseMatcher { public static final String L10N_PATH = "/org/sonar/l10n/"; - private static final String GITHUB_RAW_FILE_PATH = "https://raw.github.com/SonarSource/sonar/master/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/"; private static final Collection CORE_BUNDLES = Lists.newArrayList("checkstyle.properties", "core.properties", - "findbugs.properties", "gwt.properties", "pmd.properties", "squidjava.properties"); + "findbugs.properties", "gwt.properties", "pmd.properties", "squidjava.properties"); + private static final String GITHUB_RAW_FILE_PATH = "https://raw.github.com/SonarSource/sonar/master/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/"; private String sonarVersion; + private URI referenceEnglishBundleURI; // we use this variable to be able to unit test this class without looking at the real Github core bundles that change all the time private String remote_file_path; private String bundleName; private SortedMap missingKeys; private SortedMap additionalKeys; + public BundleSynchronizedMatcher() { + this(null, GITHUB_RAW_FILE_PATH); + } + + public BundleSynchronizedMatcher(URI referenceEnglishBundleURI) { + this.referenceEnglishBundleURI = referenceEnglishBundleURI; + } + public BundleSynchronizedMatcher(String sonarVersion) { this(sonarVersion, GITHUB_RAW_FILE_PATH); } - public BundleSynchronizedMatcher(String sonarVersion, String remote_file_path) { + @VisibleForTesting + BundleSynchronizedMatcher(String sonarVersion, String remote_file_path) { this.sonarVersion = sonarVersion; this.remote_file_path = remote_file_path; } @@ -75,6 +94,8 @@ public class BundleSynchronizedMatcher extends BaseMatcher { File defaultBundle; if (isCoreBundle(defaultBundleName)) { defaultBundle = getBundleFileFromGithub(defaultBundleName); + } else if (referenceEnglishBundleURI != null) { + defaultBundle = getBundleFileFromProvidedURI(defaultBundleName); } else { defaultBundle = getBundleFileFromClasspath(defaultBundleName); } @@ -167,18 +188,14 @@ public class BundleSynchronizedMatcher extends BaseMatcher { } protected File getBundleFileFromGithub(String defaultBundleName) { - File localBundle = new File("target/l10n/download/" + defaultBundleName); + String remoteFile = computeGitHubURL(defaultBundleName, sonarVersion); + URL remoteFileURL = null; try { - String remoteFile = computeGitHubURL(defaultBundleName, sonarVersion); - saveUrlToLocalFile(remoteFile, localBundle); + remoteFileURL = new URL(remoteFile); } catch (MalformedURLException e) { fail("Could not download the original core bundle at: " + remote_file_path + defaultBundleName); - } catch (IOException e) { - fail("Could not download the original core bundle at: " + remote_file_path + defaultBundleName); } - assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", localBundle, notNullValue()); - assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", localBundle.exists(), is(true)); - return localBundle; + return downloadRemoteFile(defaultBundleName, remoteFileURL); } protected String computeGitHubURL(String defaultBundleName, String sonarVersion) { @@ -196,18 +213,29 @@ public class BundleSynchronizedMatcher extends BaseMatcher { return bundle; } - protected String extractDefaultBundleName(String bundleName) { - int firstUnderScoreIndex = bundleName.indexOf('_'); - assertThat("The bundle '" + bundleName + "' is a default bundle (without locale), so it can't be compared.", firstUnderScoreIndex > 0, - is(true)); - return bundleName.substring(0, firstUnderScoreIndex) + ".properties"; + private File getBundleFileFromProvidedURI(String defaultBundleName) { + URL remoteFileURL = null; + try { + remoteFileURL = referenceEnglishBundleURI.toURL(); + } catch (MalformedURLException e) { + fail("Could not download the original bundle at: " + remote_file_path + defaultBundleName); + } + return downloadRemoteFile(defaultBundleName, remoteFileURL); } - protected boolean isCoreBundle(String defaultBundleName) { - return CORE_BUNDLES.contains(defaultBundleName); + private File downloadRemoteFile(String defaultBundleName, URL remoteFileUrl) { + File localBundle = new File("target/l10n/download/" + defaultBundleName); + try { + saveUrlToLocalFile(remoteFileUrl, localBundle); + } catch (IOException e) { + fail("Could not download the original core bundle at: " + remoteFileUrl.toString() + defaultBundleName); + } + assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", localBundle, notNullValue()); + assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", localBundle.exists(), is(true)); + return localBundle; } - private void saveUrlToLocalFile(String url, File localFile) throws IOException { + private void saveUrlToLocalFile(URL url, File localFile) throws IOException { if (localFile.exists()) { localFile.delete(); } @@ -216,7 +244,7 @@ public class BundleSynchronizedMatcher extends BaseMatcher { InputStream in = null; OutputStream fout = null; try { - in = new BufferedInputStream(new URL(url).openStream()); + in = new BufferedInputStream(url.openStream()); fout = new FileOutputStream(localFile); byte data[] = new byte[1024]; @@ -230,4 +258,15 @@ public class BundleSynchronizedMatcher extends BaseMatcher { } } + public static String extractDefaultBundleName(String bundleName) { + int firstUnderScoreIndex = bundleName.indexOf('_'); + assertThat("The bundle '" + bundleName + "' is a default bundle (without locale), so it can't be compared.", firstUnderScoreIndex > 0, + is(true)); + return bundleName.substring(0, firstUnderScoreIndex) + ".properties"; + } + + public static boolean isCoreBundle(String defaultBundleName) { + return CORE_BUNDLES.contains(defaultBundleName); + } + } diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java index f053ae11621..ea4ac5f5965 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java @@ -19,18 +19,19 @@ */ package org.sonar.test.i18n; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import com.google.common.collect.Maps; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.test.TestUtils; import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Collection; import java.util.Map; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.StringUtils; -import org.sonar.test.TestUtils; - -import com.google.common.collect.Maps; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; public final class I18nMatchers { @@ -38,6 +39,9 @@ public final class I18nMatchers { } /** + *

+ * Used by language packs that translate Core bundles. + *

* Returns a matcher which checks that a translation bundle is up to date with the corresponding English Core bundle. *
    *
  • If a version of Sonar is specified, then the check is done against this version of the bundle found on Sonar Github repository.
  • @@ -53,46 +57,109 @@ public final class I18nMatchers { } /** - * Returns a matcher which checks that a translation bundle is up to date with the corresponding default one found in the same folder.
    - *
    - * This matcher is used for Sonar plugins that embed their own translations. + *

    + * Used by language packs that translate third-party bundles. + *

    + * Returns a matcher which checks that a translation bundle is up to date with the given reference English bundle from a third-party plugin. + * + * @param referenceEnglishBundleURI + * the URI referencing the English bundle to check against + * @return the matcher + */ + public static BundleSynchronizedMatcher isBundleUpToDate(URI referenceEnglishBundleURI) { + return new BundleSynchronizedMatcher(referenceEnglishBundleURI); + } + + /** + *

    + * Used only by independent plugins that embeds their own bundles for every language. + *

    + * Returns a matcher which checks that a translation bundle is up to date with the corresponding default one found in the same folder. * * @return the matcher */ public static BundleSynchronizedMatcher isBundleUpToDate() { - return new BundleSynchronizedMatcher(null); + return new BundleSynchronizedMatcher(); + } + + /** + *

    + * Must be used only by independent plugins that embeds their own bundles for every language. + *

    + * Checks that all the translation bundles found on the classpath are up to date with the corresponding default one found in the same + * folder. + */ + public static void assertAllBundlesUpToDate() { + try { + assertAllBundlesUpToDate(null, null); + } catch (URISyntaxException e) { + // Ignore, this can't happen here + } } /** - * Checks that all the Core translation bundles found on the classpath are up to date with the corresponding English ones. + *

    + * Must be used only by language packs. + *

    + *

    + * Depending on the parameters, this method does the following: *

      - *
    • If a version of Sonar is specified, then the check is done against this version of the bundles found on Sonar Github repository.
    • - *
    • If sonarVersion is set to NULL, the check is done against the latest version of this bundles found on Github (master branch).
    • + *
    • sonarVersion: checks that all the Core translation bundles found on the classpath are up to date with the corresponding English ones found on Sonar + * GitHub repository for the given Sonar version. + *
      • Note: if sonarVersion is set to NULL, the check is done against the latest version of this bundles found the master branch of the GitHub repository.
      + *
    • + *
    • pluginIdsToBundleUrlMap: checks that other translation bundles found on the classpath are up to date with the reference English bundles of the corresponding + * plugins given by the "pluginIdsToBundleUrlMap" parameter. + *
    • *
    + *

    + *


    + * The following example will check that the translation of the Core bundles are up to date with version 3.2 of Sonar English Language Pack, and it + * will also check that the translation of the bundle of the Web plugin is up to date with the reference English bundle of version 1.2 of the Web plugin: + *

    +   * Map pluginIdsToBundleUrlMap = Maps.newHashMap();
    +   * pluginIdsToBundleUrlMap.put("web", "http://svn.codehaus.org/sonar-plugins/tags/sonar-web-plugin-1.2/src/main/resources/org/sonar/l10n/web.properties");
    +   * assertAllBundlesUpToDate("3.2", pluginIdsToBundleUrlMap);
    +   * 
    + *

    * * @param sonarVersion * the version of the bundles to check against, or NULL to check against the latest source on GitHub + * @param pluginIdsToBundleUrlMap + * a map that gives, for a given plugin, the URL of the English bundle that must be used to check the translation. + * @throws URISyntaxException if the provided URLs in the "pluginIdsToBundleUrlMap" parameter are not correct */ - public static void assertAllBundlesUpToDate(String sonarVersion) { + public static void assertAllBundlesUpToDate(String sonarVersion, Map pluginIdsToBundleUrlMap) throws URISyntaxException { File bundleFolder = TestUtils.getResource(BundleSynchronizedMatcher.L10N_PATH); if (bundleFolder == null || !bundleFolder.isDirectory()) { fail("No bundle found in '" + BundleSynchronizedMatcher.L10N_PATH + "'"); } - Collection bundles = FileUtils.listFiles(bundleFolder, new String[] { "properties" }, false); + Collection bundles = FileUtils.listFiles(bundleFolder, new String[] {"properties"}, false); Map failedAssertionMessages = Maps.newHashMap(); for (File bundle : bundles) { String bundleName = bundle.getName(); if (bundleName.indexOf('_') > 0) { try { - assertThat(bundleName, isBundleUpToDate(sonarVersion)); + String baseBundleName = BundleSynchronizedMatcher.extractDefaultBundleName(bundleName); + String pluginId = StringUtils.substringBefore(baseBundleName, "."); + if (BundleSynchronizedMatcher.isCoreBundle(baseBundleName)) { + // this is a core bundle => must be checked againt the provided version of Sonar + assertThat(bundleName, isBundleUpToDate(sonarVersion)); + } else if (pluginIdsToBundleUrlMap != null && pluginIdsToBundleUrlMap.get(pluginId) != null) { + // this is a third-party plugin translated by a language pack => must be checked against the provided URL + assertThat(bundleName, isBundleUpToDate(new URI(pluginIdsToBundleUrlMap.get(pluginId)))); + } else { + // this is the case of a plugin that provides all the bundles for every language => check the bundles inside the plugin + assertThat(bundleName, isBundleUpToDate()); + } } catch (AssertionError e) { failedAssertionMessages.put(bundleName, e.getMessage()); } } } - if ( !failedAssertionMessages.isEmpty()) { + if (!failedAssertionMessages.isEmpty()) { StringBuilder message = new StringBuilder(); message.append(failedAssertionMessages.size()); message.append(" bundles are not up-to-date: "); @@ -102,13 +169,4 @@ public final class I18nMatchers { fail(message.toString()); } } - - /** - * Checks that all the translation bundles found on the classpath are up to date with the corresponding default one found in the same - * folder. - */ - public static void assertAllBundlesUpToDate() { - assertAllBundlesUpToDate(null); - } - } diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java index 8554d464aae..396f196927e 100644 --- a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java +++ b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java @@ -24,50 +24,56 @@ import org.junit.Test; import org.sonar.test.TestUtils; import java.io.File; +import java.net.URI; import java.util.SortedMap; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.sonar.test.i18n.I18nMatchers.isBundleUpToDate; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; public class BundleSynchronizedTest { - private static final String GITHUB_RAW_FILE_PATH = "https://raw.github.com/SonarSource/sonar/master/sonar-testing-harness/src/test/resources/org/sonar/l10n/"; + private static final String GITHUB_RAW_TESTING_FILE_PATH = "https://raw.github.com/SonarSource/sonar/master/sonar-testing-harness/src/test/resources/org/sonar/l10n/"; private BundleSynchronizedMatcher matcher; @Before - public void test() throws Exception { - matcher = new BundleSynchronizedMatcher(null); + public void init() { + matcher = new BundleSynchronizedMatcher(); } @Test - // The case of a Sonar plugin that embeds all the bundles for every language - public void testBundlesInsideSonarPlugin() { + // The case of a Sonar Language Pack that translates the Core bundles + public void shouldMatchBundlesOfLanguagePack() { // synchronized bundle - assertThat("myPlugin_fr_CA.properties", isBundleUpToDate()); - assertFalse(new File("target/l10n/myPlugin_fr_CA.properties.report.txt").exists()); + assertThat("core_fr_CA.properties", new BundleSynchronizedMatcher(null, GITHUB_RAW_TESTING_FILE_PATH)); // missing keys try { - assertThat("myPlugin_fr.properties", isBundleUpToDate()); - assertTrue(new File("target/l10n/myPlugin_fr.properties.report.txt").exists()); + assertThat("core_fr.properties", new BundleSynchronizedMatcher(null, GITHUB_RAW_TESTING_FILE_PATH)); } catch (AssertionError e) { assertThat(e.getMessage(), containsString("Missing translations are:\nsecond.prop")); } } @Test - public void shouldNotFailIfNoMissingKeysButAdditionalKeys() { - assertThat("noMissingKeys_fr.properties", isBundleUpToDate()); + // The case of a Sonar Language Pack that translates plugin bundles + public void shouldMatchWithProvidedURI() throws Exception { + matcher = new BundleSynchronizedMatcher(new URI("http://svn.codehaus.org/sonar-plugins/tags/sonar-abacus-plugin-0.1/src/main/resources/org/sonar/l10n/abacus.properties")); + assertThat("abacus_fr.properties", matcher); } @Test - // The case of a Sonar Language Pack that translates the Core bundles - public void testBundlesOfLanguagePack() { + // The case of a Sonar plugin that embeds all the bundles for every language + public void testBundlesInsideSonarPlugin() { // synchronized bundle - assertThat("core_fr_CA.properties", new BundleSynchronizedMatcher(null, GITHUB_RAW_FILE_PATH)); + assertThat("myPlugin_fr_CA.properties", matcher); + assertFalse(new File("target/l10n/myPlugin_fr_CA.properties.report.txt").exists()); // missing keys try { - assertThat("core_fr.properties", new BundleSynchronizedMatcher(null, GITHUB_RAW_FILE_PATH)); + assertThat("myPlugin_fr.properties", matcher); + assertTrue(new File("target/l10n/myPlugin_fr.properties.report.txt").exists()); } catch (AssertionError e) { assertThat(e.getMessage(), containsString("Missing translations are:\nsecond.prop")); } @@ -85,7 +91,7 @@ public class BundleSynchronizedTest { @Test public void testGetBundleFileFromGithub() throws Exception { - matcher = new BundleSynchronizedMatcher(null, GITHUB_RAW_FILE_PATH); + matcher = new BundleSynchronizedMatcher(null, GITHUB_RAW_TESTING_FILE_PATH); matcher.getBundleFileFromGithub("core.properties"); assertTrue(new File("target/l10n/download/core.properties").exists()); } @@ -133,4 +139,5 @@ public class BundleSynchronizedTest { assertThat(matcher.computeGitHubURL("core.properties", "2.10"), is("https://raw.github.com/SonarSource/sonar/2.10/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties")); } + } diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/I18nMatchersTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/I18nMatchersTest.java new file mode 100644 index 00000000000..c97cb3951e5 --- /dev/null +++ b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/I18nMatchersTest.java @@ -0,0 +1,52 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.test.i18n; + +import org.junit.Test; + +import java.io.File; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.sonar.test.i18n.I18nMatchers.isBundleUpToDate; + +public class I18nMatchersTest { + + @Test + public void testBundlesInsideSonarPlugin() { + // synchronized bundle + assertThat("myPlugin_fr_CA.properties", isBundleUpToDate()); + assertFalse(new File("target/l10n/myPlugin_fr_CA.properties.report.txt").exists()); + // missing keys + try { + assertThat("myPlugin_fr.properties", isBundleUpToDate()); + assertTrue(new File("target/l10n/myPlugin_fr.properties.report.txt").exists()); + } catch (AssertionError e) { + assertThat(e.getMessage(), containsString("Missing translations are:\nsecond.prop")); + } + } + + @Test + public void shouldNotFailIfNoMissingKeysButAdditionalKeys() { + assertThat("noMissingKeys_fr.properties", isBundleUpToDate()); + } +} diff --git a/sonar-testing-harness/src/test/resources/org/sonar/l10n/abacus_fr.properties b/sonar-testing-harness/src/test/resources/org/sonar/l10n/abacus_fr.properties new file mode 100644 index 00000000000..03ee2266df9 --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/l10n/abacus_fr.properties @@ -0,0 +1,28 @@ +## -------- Test file for the BundleSynchronizedMatcher -------- ## +widget.abacus.name=Abaques +widget.abacus.description=Calcule la complexit\u00e9 de chaque composant pour vous aider \u00e0 utiliser vos abaques. + +widget.abacus.param.defaultColors=Liste de couleurs (format hexad\u00e9cimal) s\u00e9par\u00e9e par des virgules pour l'affichage du camembert.
    Bien faire attention \u00e0 ce que le nombre de couleurs soit au minimum le m\u00eame que le nombre de niveaux de complexit\u00e9 des abaques. +widget.abacus.param.defaultDisplay=Valeurs possibles :
    • files (= nombre de fichiers)
    • percentage (= pourcentage)
    + +abacusTab.page=Abaques +abacusTab.notComputed=Non calcul\u00e9e + +property.sonar.abacus.complexityThresholds.name=Seuils de complexit\u00e9 des abaques +property.sonar.abacus.complexityThresholds.description=Usage : NomSeuil1:Complexit\u00e9Seuil1;NomSeuil2:Complexit\u00e9Seuil2;...;NomSeuilN
    Valeur par d\u00e9faut : Simple:20;Medium:50;Complex:100;Very Complex + +metric.abacus-complexity.name=Complexit\u00e9 (Abaques) +metric.abacus-complexity.description=Complexit\u00e9 (Abaques) +metric.abacus-complexity-distribution.name=Distribution de la complexit\u00e9 (Abaques) +metric.abacus-complexity-distribution.description=Distribution de la complexit\u00e9 (Abaques) + +abacus.componentComplexity=Complexit\u00e9 du composant +abacus.componentComplexityDistribution.numberOfFiles=Distrib. complexit\u00e9 (fichiers) +abacus.componentComplexityDistribution.percentage=Distrib. complexit\u00e9 (%) +abacus.error.cannotDisplayWidget=Impossible d'afficher le widget. +abacus.error.defaultColors.incorrectValue=La valeur du param\u00e8tre de wigdet defaultColors est incorrecte : moins de couleurs que de niveaux de complexit\u00e9 dans les abaques. +abacus.error.defaultDisplay.incorrectValue=La valeur du param\u00e8tre de widget defaultDisplay est incorrecte : {0}
    Valeurs possibles :
    • files (= nombre de fichiers)
    • percentage (= pourcentage)
    +abacus.noData=Pas de donn\u00e9es +abacus.numberOfFilesDistribution=Distribution en nombre de fichiers +abacus.percentageDistribution=Distribution en % +abacus.title=Abaques \ No newline at end of file