Merge pull request #1586 from hapifhir/do-20240327-language-translation-coverage
Add coverage test to output missing translations + summary
This commit is contained in:
commit
f9aaf9a1f3
|
@ -0,0 +1,34 @@
|
|||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
#define figure and axes
|
||||
fig, ax = plt.subplots(1,1)
|
||||
|
||||
#hide the axes
|
||||
fig.patch.set_visible(False)
|
||||
ax.axis('off')
|
||||
ax.axis('tight')
|
||||
|
||||
#read data
|
||||
df = pd.read_csv('i18n-coverage.csv')
|
||||
#create table
|
||||
table = ax.table(cellText=df.values, colLabels=df.columns, loc='center')
|
||||
|
||||
table.scale(1, 4)
|
||||
table.auto_set_font_size(False)
|
||||
table.set_fontsize(14)
|
||||
|
||||
fig.tight_layout()
|
||||
fig.set_figheight(2)
|
||||
fig.set_figwidth(4)
|
||||
|
||||
|
||||
ax.set_title('Internationalization Phrase Coverage by Locale')
|
||||
|
||||
fig = plt.gcf()
|
||||
|
||||
plt.savefig('i18n-coverage-table.png',
|
||||
bbox_inches='tight',
|
||||
dpi=150
|
||||
)
|
|
@ -25,6 +25,6 @@ jobs:
|
|||
id: bidi_check
|
||||
uses: HL7/bidi-checker-action@v1.9
|
||||
env:
|
||||
IGNORE: dummy-package.tgz$
|
||||
IGNORE: i18n-coverage-table\.png$|dummy-package.tgz$
|
||||
- name: Get the output time
|
||||
run: echo "The time was ${{ steps.bidi_check.outputs.time }}"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Locale,Complete #,Complete %
|
||||
de,860,78%
|
||||
es,731,66%
|
||||
ja,910,82%
|
||||
nl,853,77%
|
|
|
@ -1,10 +1,15 @@
|
|||
# We only want to trigger a test build on PRs to the main branch.
|
||||
trigger: none
|
||||
# This pipeline runs the internationalization coverage test and then uses a
|
||||
# python script to generate a table from the results for viewing in the
|
||||
# README.md file
|
||||
pr: none
|
||||
|
||||
pr:
|
||||
trigger:
|
||||
- master
|
||||
|
||||
variables:
|
||||
# Normally this test outputs to console. This variable appears as env param
|
||||
# I18N_COVERAGE_FILE, which tells the test to write the output to a file
|
||||
# instead.
|
||||
- name: i18n.coverage.file
|
||||
value: i18n-coverage.csv
|
||||
- group: PGP_VAR_GROUP
|
||||
|
@ -39,6 +44,8 @@ jobs:
|
|||
jdkVersionOption: '1.11'
|
||||
jdkArchitectureOption: 'x64'
|
||||
goals: 'install'
|
||||
displayName: 'Build utilities module'
|
||||
|
||||
- task: Maven@3
|
||||
inputs:
|
||||
mavenPomFile: 'pom.xml'
|
||||
|
@ -48,9 +55,27 @@ jobs:
|
|||
jdkVersionOption: '1.11'
|
||||
jdkArchitectureOption: 'x64'
|
||||
goals: 'surefire:test'
|
||||
displayName: 'Run i18n coverage test to generate csv'
|
||||
|
||||
- task: PythonScript@0
|
||||
inputs:
|
||||
scriptSource: 'filePath'
|
||||
scriptPath: .azure/i18n-coverage-table/generate-i18n-coverage-table.py
|
||||
arguments:
|
||||
displayName: 'Make png table from coverage test csv'
|
||||
|
||||
# Verify png file generation
|
||||
- bash: |
|
||||
ls -l ./i18n-coverage-table.png
|
||||
|
||||
- bash: |
|
||||
git fetch
|
||||
git checkout master
|
||||
git status
|
||||
git add ./i18n-coverage.csv
|
||||
git add ./i18n-coverage-table.png
|
||||
git commit . -m "Updating i18n-coverage csv and png table ***NO_CI***"
|
||||
|
||||
git push https://$(GIT_PAT)@github.com/hapifhir/org.hl7.fhir.core.git
|
||||
|
||||
displayName: 'Push updated csv and plot to git.'
|
|
@ -1,10 +1,7 @@
|
|||
package org.hl7.fhir.utilities.i18n;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
@ -98,6 +95,10 @@ public abstract class I18nBase {
|
|||
.map(entry -> baseKey + KEY_DELIMITER + entry).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
protected Set<String> getPluralSuffixes() {
|
||||
return Collections.unmodifiableSet(pluralRules.getKeywords());
|
||||
}
|
||||
|
||||
protected String getRootKeyFromPlural(@Nonnull String pluralKey) {
|
||||
checkPluralRulesAreLoaded();
|
||||
for (String keyword : pluralRules
|
||||
|
|
|
@ -3,13 +3,12 @@ package org.hl7.fhir.utilities.i18n;
|
|||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
|
@ -17,8 +16,6 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
public class I18nCoverageTest {
|
||||
|
||||
|
||||
|
||||
final Set<Locale> locales = Set.of(
|
||||
Locale.ENGLISH,
|
||||
Locale.GERMAN,
|
||||
|
@ -28,7 +25,128 @@ public class I18nCoverageTest {
|
|||
);
|
||||
|
||||
@Test
|
||||
public void testCoverage() throws IllegalAccessException {
|
||||
public void testPhraseCoverage() throws IOException {
|
||||
|
||||
Properties englishMessages = new Properties();
|
||||
englishMessages.load(I18nTestClass.class.getClassLoader().getResourceAsStream("Messages.properties"));
|
||||
I18nTestClass englishTestClass = getI18nTestClass(Locale.ENGLISH);
|
||||
Set<String> englishPluralSuffixes = englishTestClass.getPluralSuffixes();
|
||||
|
||||
Set<String> englishPluralKeys = new HashSet<>();
|
||||
Set<String> englishKeys = new HashSet<>();
|
||||
for (Object objectKey : englishMessages.keySet()) {
|
||||
String key = (String) objectKey;
|
||||
if (isPluralKey(key, englishPluralSuffixes)) {
|
||||
final String pluralKeyRoot = getPluralKeyRoot(key, englishPluralSuffixes);
|
||||
englishPluralKeys.add(pluralKeyRoot);
|
||||
} else {
|
||||
englishKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
HashMap<Locale, Integer> foundKeys = new HashMap<>();
|
||||
HashMap<Locale, Integer> foundPluralKeys = new HashMap<>();
|
||||
|
||||
for (Locale locale : locales) {
|
||||
if (!locale.equals(Locale.ENGLISH)) {
|
||||
Properties translatedMessages = new Properties();
|
||||
translatedMessages.load(I18nTestClass.class.getClassLoader().getResourceAsStream("Messages_" + locale.toString() + ".properties"));
|
||||
I18nTestClass translatedTestClass = getI18nTestClass(Locale.ENGLISH);
|
||||
Set<String> translatedPluralSuffixes = translatedTestClass.getPluralSuffixes();
|
||||
|
||||
Set<String> translatedPluralKeys = new HashSet<>();
|
||||
Set<String> translatedKeys = new HashSet<>();
|
||||
|
||||
for (Object objectKey : translatedMessages.keySet()) {
|
||||
String key = (String) objectKey;
|
||||
Object value = translatedMessages.get(objectKey);
|
||||
if (
|
||||
value instanceof String &&
|
||||
!((String) value).trim().isEmpty()) {
|
||||
if (isPluralKey(key, translatedPluralSuffixes)) {
|
||||
final String pluralKeyRoot = getPluralKeyRoot(key, englishPluralSuffixes);
|
||||
translatedPluralKeys.add(pluralKeyRoot);
|
||||
} else {
|
||||
translatedKeys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> intersectionKeys = new HashSet<>(englishKeys);
|
||||
intersectionKeys.retainAll(translatedKeys);
|
||||
Set<String> intersectionPluralKeys = new HashSet<>(englishPluralKeys);
|
||||
intersectionPluralKeys.retainAll(translatedPluralKeys);
|
||||
|
||||
Set<String> missingKeys = new HashSet<>(englishKeys);
|
||||
Set<String> missingPluralKeys = new HashSet<>(englishPluralKeys);
|
||||
|
||||
missingKeys.removeAll(translatedKeys);
|
||||
missingPluralKeys.removeAll(translatedPluralKeys);
|
||||
|
||||
foundKeys.put(locale, intersectionKeys.size());
|
||||
foundPluralKeys.put(locale, intersectionPluralKeys.size());
|
||||
|
||||
for (String missingKey : missingKeys) {
|
||||
System.err.println("Missing key for locale " + locale + ": " + missingKey);
|
||||
}
|
||||
for (String missingPluralKey : missingPluralKeys) {
|
||||
System.err.println("Missing plural key for locale " + locale + ": " + missingPluralKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PrintStream out = getCSVOutputStream();
|
||||
|
||||
printPhraseCoverageCSV(out, foundKeys, foundPluralKeys, englishKeys, englishPluralKeys);
|
||||
}
|
||||
|
||||
private static PrintStream getCSVOutputStream() throws FileNotFoundException {
|
||||
String outputFile = System.getenv("I18N_COVERAGE_FILE");
|
||||
|
||||
return outputFile == null
|
||||
? System.out
|
||||
: new PrintStream(new File(outputFile));
|
||||
}
|
||||
|
||||
private static void printPhraseCoverageCSV(PrintStream out, HashMap<Locale, Integer> foundKeys, HashMap<Locale, Integer> foundPluralKeys, Set<String> englishKeys, Set<String> englishPluralKeys) {
|
||||
out.println("Locale,Complete #,Complete %");
|
||||
for (Locale locale : foundKeys.keySet()) {
|
||||
int singleCount = foundKeys.get(locale);
|
||||
int pluralCount = foundPluralKeys.get(locale);
|
||||
|
||||
int count = singleCount + pluralCount;
|
||||
int total = englishKeys.size() + englishPluralKeys.size();
|
||||
|
||||
out.println(locale + "," + count + "," + getPercent( count, total));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getPercent(int numerator, int denominator) {
|
||||
return (int) (((double)numerator / denominator) * 100) + "%";
|
||||
}
|
||||
|
||||
private String getPluralKeyRoot(String key, Set<String> pluralKeys) {
|
||||
for (String pluralKey : pluralKeys) {
|
||||
final String suffix = I18nBase.KEY_DELIMITER + pluralKey;
|
||||
if (key.endsWith(suffix)) {
|
||||
return key.substring(0, key.lastIndexOf(suffix));
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(key + " does not terminate with a plural suffix. Available: " + pluralKeys);
|
||||
}
|
||||
|
||||
private boolean isPluralKey(String key, Set<String> pluralKeys) {
|
||||
for (String pluralKey : pluralKeys) {
|
||||
if (key.endsWith(I18nBase.KEY_DELIMITER + pluralKey)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstantsCoverage() throws IllegalAccessException {
|
||||
|
||||
Field[] fields = I18nConstants.class.getDeclaredFields();
|
||||
Map<Locale, I18nBase> testClassMap = new HashMap<>();
|
||||
|
@ -49,7 +167,6 @@ public class I18nCoverageTest {
|
|||
for (Locale locale : locales) {
|
||||
I18nBase base = testClassMap.get(locale);
|
||||
|
||||
|
||||
isSingularPhrase.put(locale, base.messageKeyExistsForLocale(message));
|
||||
isPluralPhrase.put(locale, existsAsPluralPhrase(base, message));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue