Skip to content

Commit f9aaf9a

Browse files
Merge pull request hapifhir#1586 from hapifhir/do-20240327-language-translation-coverage
Add coverage test to output missing translations + summary
2 parents 9372e62 + f029c7b commit f9aaf9a

File tree

6 files changed

+200
-18
lines changed

6 files changed

+200
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import numpy as np
2+
import pandas as pd
3+
import matplotlib.pyplot as plt
4+
5+
#define figure and axes
6+
fig, ax = plt.subplots(1,1)
7+
8+
#hide the axes
9+
fig.patch.set_visible(False)
10+
ax.axis('off')
11+
ax.axis('tight')
12+
13+
#read data
14+
df = pd.read_csv('i18n-coverage.csv')
15+
#create table
16+
table = ax.table(cellText=df.values, colLabels=df.columns, loc='center')
17+
18+
table.scale(1, 4)
19+
table.auto_set_font_size(False)
20+
table.set_fontsize(14)
21+
22+
fig.tight_layout()
23+
fig.set_figheight(2)
24+
fig.set_figwidth(4)
25+
26+
27+
ax.set_title('Internationalization Phrase Coverage by Locale')
28+
29+
fig = plt.gcf()
30+
31+
plt.savefig('i18n-coverage-table.png',
32+
bbox_inches='tight',
33+
dpi=150
34+
)

.github/workflows/bidi-checker.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ jobs:
2525
id: bidi_check
2626
2727
env:
28-
IGNORE: dummy-package.tgz$
28+
IGNORE: i18n-coverage-table\.png$|dummy-package.tgz$
2929
- name: Get the output time
3030
run: echo "The time was ${{ steps.bidi_check.outputs.time }}"

i18n-coverage.csv

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Locale,Complete #,Complete %
2+
de,860,78%
3+
es,731,66%
4+
ja,910,82%
5+
nl,853,77%

master-branch-i18n-coverage-pipeline.yml

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
# We only want to trigger a test build on PRs to the main branch.
2-
trigger: none
1+
# This pipeline runs the internationalization coverage test and then uses a
2+
# python script to generate a table from the results for viewing in the
3+
# README.md file
4+
pr: none
35

4-
pr:
6+
trigger:
57
- master
68

79
variables:
10+
# Normally this test outputs to console. This variable appears as env param
11+
# I18N_COVERAGE_FILE, which tells the test to write the output to a file
12+
# instead.
813
- name: i18n.coverage.file
914
value: i18n-coverage.csv
1015
- group: PGP_VAR_GROUP
@@ -39,6 +44,8 @@ jobs:
3944
jdkVersionOption: '1.11'
4045
jdkArchitectureOption: 'x64'
4146
goals: 'install'
47+
displayName: 'Build utilities module'
48+
4249
- task: Maven@3
4350
inputs:
4451
mavenPomFile: 'pom.xml'
@@ -48,9 +55,27 @@ jobs:
4855
jdkVersionOption: '1.11'
4956
jdkArchitectureOption: 'x64'
5057
goals: 'surefire:test'
58+
displayName: 'Run i18n coverage test to generate csv'
5159

5260
- task: PythonScript@0
5361
inputs:
5462
scriptSource: 'filePath'
5563
scriptPath: .azure/i18n-coverage-table/generate-i18n-coverage-table.py
5664
arguments:
65+
displayName: 'Make png table from coverage test csv'
66+
67+
# Verify png file generation
68+
- bash: |
69+
ls -l ./i18n-coverage-table.png
70+
71+
- bash: |
72+
git fetch
73+
git checkout master
74+
git status
75+
git add ./i18n-coverage.csv
76+
git add ./i18n-coverage-table.png
77+
git commit . -m "Updating i18n-coverage csv and png table ***NO_CI***"
78+
79+
git push https://$(GIT_PAT)@github.com/hapifhir/org.hl7.fhir.core.git
80+
81+
displayName: 'Push updated csv and plot to git.'

org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nBase.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package org.hl7.fhir.utilities.i18n;
22

33
import java.text.MessageFormat;
4-
import java.util.Locale;
5-
import java.util.Objects;
6-
import java.util.ResourceBundle;
7-
import java.util.Set;
4+
import java.util.*;
85
import java.util.stream.Collectors;
96

107
import javax.annotation.Nonnull;
@@ -98,6 +95,10 @@ protected Set<String> getPluralKeys(String baseKey) {
9895
.map(entry -> baseKey + KEY_DELIMITER + entry).collect(Collectors.toSet());
9996
}
10097

98+
protected Set<String> getPluralSuffixes() {
99+
return Collections.unmodifiableSet(pluralRules.getKeywords());
100+
}
101+
101102
protected String getRootKeyFromPlural(@Nonnull String pluralKey) {
102103
checkPluralRulesAreLoaded();
103104
for (String keyword : pluralRules

org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/i18n/I18nCoverageTest.java

+127-10
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,19 @@
33
import static org.junit.jupiter.api.Assertions.assertFalse;
44
import static org.junit.jupiter.api.Assertions.assertTrue;
55

6+
import java.io.File;
7+
import java.io.FileNotFoundException;
8+
import java.io.IOException;
9+
import java.io.PrintStream;
610
import java.lang.reflect.Field;
7-
import java.util.HashMap;
8-
import java.util.HashSet;
9-
import java.util.Locale;
10-
import java.util.Map;
11-
import java.util.ResourceBundle;
12-
import java.util.Set;
11+
import java.util.*;
1312

1413
import javax.annotation.Nonnull;
1514

1615
import org.junit.jupiter.api.Test;
1716

1817
public class I18nCoverageTest {
1918

20-
21-
2219
final Set<Locale> locales = Set.of(
2320
Locale.ENGLISH,
2421
Locale.GERMAN,
@@ -28,7 +25,128 @@ public class I18nCoverageTest {
2825
);
2926

3027
@Test
31-
public void testCoverage() throws IllegalAccessException {
28+
public void testPhraseCoverage() throws IOException {
29+
30+
Properties englishMessages = new Properties();
31+
englishMessages.load(I18nTestClass.class.getClassLoader().getResourceAsStream("Messages.properties"));
32+
I18nTestClass englishTestClass = getI18nTestClass(Locale.ENGLISH);
33+
Set<String> englishPluralSuffixes = englishTestClass.getPluralSuffixes();
34+
35+
Set<String> englishPluralKeys = new HashSet<>();
36+
Set<String> englishKeys = new HashSet<>();
37+
for (Object objectKey : englishMessages.keySet()) {
38+
String key = (String) objectKey;
39+
if (isPluralKey(key, englishPluralSuffixes)) {
40+
final String pluralKeyRoot = getPluralKeyRoot(key, englishPluralSuffixes);
41+
englishPluralKeys.add(pluralKeyRoot);
42+
} else {
43+
englishKeys.add(key);
44+
}
45+
}
46+
47+
HashMap<Locale, Integer> foundKeys = new HashMap<>();
48+
HashMap<Locale, Integer> foundPluralKeys = new HashMap<>();
49+
50+
for (Locale locale : locales) {
51+
if (!locale.equals(Locale.ENGLISH)) {
52+
Properties translatedMessages = new Properties();
53+
translatedMessages.load(I18nTestClass.class.getClassLoader().getResourceAsStream("Messages_" + locale.toString() + ".properties"));
54+
I18nTestClass translatedTestClass = getI18nTestClass(Locale.ENGLISH);
55+
Set<String> translatedPluralSuffixes = translatedTestClass.getPluralSuffixes();
56+
57+
Set<String> translatedPluralKeys = new HashSet<>();
58+
Set<String> translatedKeys = new HashSet<>();
59+
60+
for (Object objectKey : translatedMessages.keySet()) {
61+
String key = (String) objectKey;
62+
Object value = translatedMessages.get(objectKey);
63+
if (
64+
value instanceof String &&
65+
!((String) value).trim().isEmpty()) {
66+
if (isPluralKey(key, translatedPluralSuffixes)) {
67+
final String pluralKeyRoot = getPluralKeyRoot(key, englishPluralSuffixes);
68+
translatedPluralKeys.add(pluralKeyRoot);
69+
} else {
70+
translatedKeys.add(key);
71+
}
72+
}
73+
}
74+
75+
Set<String> intersectionKeys = new HashSet<>(englishKeys);
76+
intersectionKeys.retainAll(translatedKeys);
77+
Set<String> intersectionPluralKeys = new HashSet<>(englishPluralKeys);
78+
intersectionPluralKeys.retainAll(translatedPluralKeys);
79+
80+
Set<String> missingKeys = new HashSet<>(englishKeys);
81+
Set<String> missingPluralKeys = new HashSet<>(englishPluralKeys);
82+
83+
missingKeys.removeAll(translatedKeys);
84+
missingPluralKeys.removeAll(translatedPluralKeys);
85+
86+
foundKeys.put(locale, intersectionKeys.size());
87+
foundPluralKeys.put(locale, intersectionPluralKeys.size());
88+
89+
for (String missingKey : missingKeys) {
90+
System.err.println("Missing key for locale " + locale + ": " + missingKey);
91+
}
92+
for (String missingPluralKey : missingPluralKeys) {
93+
System.err.println("Missing plural key for locale " + locale + ": " + missingPluralKey);
94+
}
95+
}
96+
}
97+
98+
99+
PrintStream out = getCSVOutputStream();
100+
101+
printPhraseCoverageCSV(out, foundKeys, foundPluralKeys, englishKeys, englishPluralKeys);
102+
}
103+
104+
private static PrintStream getCSVOutputStream() throws FileNotFoundException {
105+
String outputFile = System.getenv("I18N_COVERAGE_FILE");
106+
107+
return outputFile == null
108+
? System.out
109+
: new PrintStream(new File(outputFile));
110+
}
111+
112+
private static void printPhraseCoverageCSV(PrintStream out, HashMap<Locale, Integer> foundKeys, HashMap<Locale, Integer> foundPluralKeys, Set<String> englishKeys, Set<String> englishPluralKeys) {
113+
out.println("Locale,Complete #,Complete %");
114+
for (Locale locale : foundKeys.keySet()) {
115+
int singleCount = foundKeys.get(locale);
116+
int pluralCount = foundPluralKeys.get(locale);
117+
118+
int count = singleCount + pluralCount;
119+
int total = englishKeys.size() + englishPluralKeys.size();
120+
121+
out.println(locale + "," + count + "," + getPercent( count, total));
122+
}
123+
}
124+
125+
private static String getPercent(int numerator, int denominator) {
126+
return (int) (((double)numerator / denominator) * 100) + "%";
127+
}
128+
129+
private String getPluralKeyRoot(String key, Set<String> pluralKeys) {
130+
for (String pluralKey : pluralKeys) {
131+
final String suffix = I18nBase.KEY_DELIMITER + pluralKey;
132+
if (key.endsWith(suffix)) {
133+
return key.substring(0, key.lastIndexOf(suffix));
134+
}
135+
}
136+
throw new IllegalArgumentException(key + " does not terminate with a plural suffix. Available: " + pluralKeys);
137+
}
138+
139+
private boolean isPluralKey(String key, Set<String> pluralKeys) {
140+
for (String pluralKey : pluralKeys) {
141+
if (key.endsWith(I18nBase.KEY_DELIMITER + pluralKey)) {
142+
return true;
143+
}
144+
}
145+
return false;
146+
}
147+
148+
@Test
149+
public void testConstantsCoverage() throws IllegalAccessException {
32150

33151
Field[] fields = I18nConstants.class.getDeclaredFields();
34152
Map<Locale, I18nBase> testClassMap = new HashMap<>();
@@ -49,7 +167,6 @@ public void testCoverage() throws IllegalAccessException {
49167
for (Locale locale : locales) {
50168
I18nBase base = testClassMap.get(locale);
51169

52-
53170
isSingularPhrase.put(locale, base.messageKeyExistsForLocale(message));
54171
isPluralPhrase.put(locale, existsAsPluralPhrase(base, message));
55172
}

0 commit comments

Comments
 (0)