This commit is contained in:
Grahame Grieve 2024-05-05 18:17:33 +10:00
commit c24fde0bb6
19 changed files with 239 additions and 51 deletions

View File

@ -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
)

View File

@ -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 }}"

View File

@ -1,21 +1,7 @@
## Validator Changes
* fix NPE loading resources
* Don't enforce ids on elements when processing CDA
* Send supplements to tx server
* fix bug processing code bindings when value sets are complex (multiple filters)
* fix spelling of heirarchy
* Look up CodeSystem from terminology server
* Don't use tx-registry when manual terminology server is set
* no changes
## Other code changes
* More work on WHO language support ($1592)
* allow validation message to have count
* render versions in profile links when necessary
* rework OID handling for better OID -> CodeSystem resolution
* fix up vsac importer for changes to client
* don't send xhtml for tx operations
* FHIRPath: Backport the defineVariable code to the R4 and R4B fhirpath implementations
* FHIRPath: Remove the alias/aliasAs custom functions (use standard defineVariable now)
* Bump lombok (#1603)
* no changes

BIN
i18n-coverage-table.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

5
i18n-coverage.csv Normal file
View File

@ -0,0 +1,5 @@
Locale,Coverage #,Coverage %
de,869,70%
es,740,59%
ja,935,75%
nl,873,70%
1 Locale Coverage # Coverage %
2 de 869 70%
3 es 740 59%
4 ja 935 75%
5 nl 873 70%

View File

@ -1,12 +1,17 @@
# 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
value: ../i18n-coverage.csv
- group: PGP_VAR_GROUP
- group: SONATYPE_VAR_GROUP
- group: GIT_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.'

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -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,7 +16,12 @@ import org.junit.jupiter.api.Test;
public class I18nCoverageTest {
private static class I18nCoverage {
final Set<String> englishKeys = new HashSet<>();
final Set<String> englishPluralKeys = new HashSet<>();
final HashMap<Locale, Integer> foundKeys = new HashMap<>();
final HashMap<Locale, Integer> foundPluralKeys = new HashMap<>();
}
final Set<Locale> locales = Set.of(
Locale.ENGLISH,
@ -27,8 +31,141 @@ public class I18nCoverageTest {
Locale.forLanguageTag("ja")
);
final Locale sourceLocale = Locale.ENGLISH;
@Test
public void testCoverage() throws IllegalAccessException {
public void testPhraseCoverage() throws IOException {
I18nCoverage messages = getI18nCoverage("Messages");
I18nCoverage renderingPhrases = getI18nCoverage("rendering-phrases");
PrintStream out = getCSVOutputStream();
printPhraseCoverageCSV(out, List.of(messages, renderingPhrases));
}
private I18nCoverage getI18nCoverage(String messageFilePrefix) throws IOException {
I18nCoverage i18nCoverage = new I18nCoverage();
Properties englishMessages = new Properties();
englishMessages.load(I18nTestClass.class.getClassLoader().getResourceAsStream(messageFilePrefix + ".properties"));
I18nTestClass englishTestClass = getI18nTestClass(Locale.ENGLISH);
Set<String> englishPluralSuffixes = englishTestClass.getPluralSuffixes();
for (Object objectKey : englishMessages.keySet()) {
String key = (String) objectKey;
if (isPluralKey(key, englishPluralSuffixes)) {
final String pluralKeyRoot = getPluralKeyRoot(key, englishPluralSuffixes);
i18nCoverage.englishPluralKeys.add(pluralKeyRoot);
} else {
i18nCoverage.englishKeys.add(key);
}
}
for (Locale locale : locales) {
if (!locale.equals(sourceLocale)) {
Properties translatedMessages = new Properties();
translatedMessages.load(I18nTestClass.class.getClassLoader().getResourceAsStream(messageFilePrefix + "_" + locale.toString() + ".properties"));
I18nTestClass translatedTestClass = getI18nTestClass(sourceLocale);
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<>(i18nCoverage.englishKeys);
intersectionKeys.retainAll(translatedKeys);
Set<String> intersectionPluralKeys = new HashSet<>(i18nCoverage.englishPluralKeys);
intersectionPluralKeys.retainAll(translatedPluralKeys);
Set<String> missingKeys = new HashSet<>(i18nCoverage.englishKeys);
Set<String> missingPluralKeys = new HashSet<>(i18nCoverage.englishPluralKeys);
missingKeys.removeAll(translatedKeys);
missingPluralKeys.removeAll(translatedPluralKeys);
i18nCoverage.foundKeys.put(locale, intersectionKeys.size());
i18nCoverage.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);
}
}
}
return i18nCoverage;
}
private static PrintStream getCSVOutputStream() throws FileNotFoundException {
String outputFile = System.getenv("I18N_COVERAGE_FILE");
return outputFile == null
? System.out
: new PrintStream(new File(outputFile));
}
private void printPhraseCoverageCSV(PrintStream out, List<I18nCoverage> i18nCoverageList) {
out.println("Locale,Coverage #,Coverage %");
List<Locale> sortedLocales = new ArrayList<>(locales);
sortedLocales.sort(Comparator.comparing(Locale::toString));
for (Locale locale : sortedLocales) {
if (!locale.equals(sourceLocale)) {
int count = 0;
int total = 0;
for (I18nCoverage i18nCoverage : i18nCoverageList) {
int singleCount = i18nCoverage.foundKeys.get(locale);
int pluralCount = i18nCoverage.foundPluralKeys.get(locale);
count += singleCount + pluralCount;
total += i18nCoverage.englishKeys.size() + i18nCoverage.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<>();
@ -40,16 +177,15 @@ public class I18nCoverageTest {
Set<String> messages = new HashSet<>();
for (Field field : fields) {
String message = (String)field.get(new String());
String message = (String) field.get(new String());
messages.add(message);
if (field.getType() == String.class) {
Map<Locale, Boolean> isSingularPhrase = new HashMap<>();
Map<Locale, Boolean> isSingularPhrase = new HashMap<>();
Map<Locale, Boolean> isPluralPhrase = new HashMap<>();
for (Locale locale : locales) {
I18nBase base = testClassMap.get(locale);
isSingularPhrase.put(locale, base.messageKeyExistsForLocale(message));
isPluralPhrase.put(locale, existsAsPluralPhrase(base, message));
}
@ -65,7 +201,7 @@ public class I18nCoverageTest {
boolean mapsToConstant = messages.contains(message);
boolean mapsToPluralPhrase = mapsToPluralPhrase(messages, message, testClassMap.get(locale));
if (!(mapsToConstant || mapsToPluralPhrase)) {
System.err.println("Message " + message + " in " + locale.getLanguage() + " properties resource does not have a matching entry in " + I18nConstants.class.getName() );
System.err.println("Message " + message + " in " + locale.getLanguage() + " properties resource does not have a matching entry in " + I18nConstants.class.getName());
}
}
}
@ -81,14 +217,14 @@ public class I18nCoverageTest {
}
private void assertPhraseTypeAgreement(Field field,
Map<Locale, Boolean> isSingularPhrase,
Map<Locale, Boolean> isPluralPhrase) {
Map<Locale, Boolean> isSingularPhrase,
Map<Locale, Boolean> isPluralPhrase) {
boolean existsAsSingular = isSingularPhrase.values().stream().anyMatch(value -> value == true);
boolean existsAsPlural = isPluralPhrase.values().stream().anyMatch(value -> value == true);
assertTrue(
//The phrase might not exist
(existsAsPlural == false && existsAsSingular == false)
// But if it does exist, it must consistently be of singular or plural
// But if it does exist, it must consistently be of singular or plural
|| existsAsPlural ^ existsAsSingular,
"Constant " + field.getName() + " has inconsistent plural properties in I18n property definitions: " + pluralPropertySummary(isSingularPhrase, isPluralPhrase));
}
@ -103,7 +239,8 @@ public class I18nCoverageTest {
if (!existsInSomeLanguage) {
System.err.println("Constant " + field.getName() + " does not exist in any I18n property definition");
return;
};
}
;
if (existsAsSingular) {
logMissingPhrases(field, isSingularPhrase, "singular");
}
@ -120,14 +257,15 @@ public class I18nCoverageTest {
}
}
private String pluralPropertySummary( Map<Locale, Boolean> isSingularPhrase,
Map<Locale, Boolean> isPluralPhrase) {
private String pluralPropertySummary(Map<Locale, Boolean> isSingularPhrase,
Map<Locale, Boolean> isPluralPhrase) {
StringBuilder stringBuilder = new StringBuilder();
for (Locale locale : locales) {
stringBuilder.append("locale: " + locale.getDisplayName() + " singular:" + isSingularPhrase.get(locale) + " plural: " + isPluralPhrase.get(locale) + ";");
}
return stringBuilder.toString();
}
@Nonnull
private static I18nTestClass getI18nTestClass(Locale locale) {
I18nTestClass testClass = new I18nTestClass();

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -14,7 +14,7 @@
HAPI FHIR
-->
<artifactId>org.hl7.fhir.core</artifactId>
<version>6.3.6</version>
<version>6.3.7-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>