Skip to content

Commit

Permalink
Merge pull request hapifhir#1586 from hapifhir/do-20240327-language-t…
Browse files Browse the repository at this point in the history
…ranslation-coverage

Add coverage test to output missing translations + summary
  • Loading branch information
grahamegrieve authored Apr 23, 2024
2 parents 9372e62 + f029c7b commit f9aaf9a
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 18 deletions.
34 changes: 34 additions & 0 deletions .azure/i18n-coverage-table/generate-i18n-coverage-table.py
Original file line number Diff line number Diff line change
@@ -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
)
2 changes: 1 addition & 1 deletion .github/workflows/bidi-checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ jobs:
id: bidi_check
uses: HL7/[email protected]
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 }}"
5 changes: 5 additions & 0 deletions i18n-coverage.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Locale,Complete #,Complete %
de,860,78%
es,731,66%
ja,910,82%
nl,853,77%
31 changes: 28 additions & 3 deletions master-branch-i18n-coverage-pipeline.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -39,6 +44,8 @@ jobs:
jdkVersionOption: '1.11'
jdkArchitectureOption: 'x64'
goals: 'install'
displayName: 'Build utilities module'

- task: Maven@3
inputs:
mavenPomFile: 'pom.xml'
Expand All @@ -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.'
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -98,6 +95,10 @@ protected Set<String> getPluralKeys(String baseKey) {
.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@
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;

import org.junit.jupiter.api.Test;

public class I18nCoverageTest {



final Set<Locale> locales = Set.of(
Locale.ENGLISH,
Locale.GERMAN,
Expand All @@ -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<>();
Expand All @@ -49,7 +167,6 @@ public void testCoverage() throws IllegalAccessException {
for (Locale locale : locales) {
I18nBase base = testClassMap.get(locale);


isSingularPhrase.put(locale, base.messageKeyExistsForLocale(message));
isPluralPhrase.put(locale, existsAsPluralPhrase(base, message));
}
Expand Down

0 comments on commit f9aaf9a

Please sign in to comment.