summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-10-19 13:45:57 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-10-21 10:10:57 +0200
commit0c6bf34d23c44f5c7c16a03f089291eb3c1876cb (patch)
treec841385cb1fc948c7c65133aa6f53bdba4fc05df
parent2ecd4228773aa06ee448e1285ffb48b9d3df2ba8 (diff)
downloadmullvadvpn-0c6bf34d23c44f5c7c16a03f089291eb3c1876cb.tar.xz
mullvadvpn-0c6bf34d23c44f5c7c16a03f089291eb3c1876cb.zip
Add check for html format to translations check
-rw-r--r--.github/workflows/translations.yml2
-rw-r--r--gui/scripts/verify-format-specifiers.ts73
-rw-r--r--gui/scripts/verify-translations-format.ts130
3 files changed, 131 insertions, 74 deletions
diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml
index c6bb477ab1..dde8b9c2fd 100644
--- a/.github/workflows/translations.yml
+++ b/.github/workflows/translations.yml
@@ -56,4 +56,4 @@ jobs:
- name: Check if format specifiers are correct
working-directory: gui/scripts
- run: npm exec ts-node verify-format-specifiers.ts
+ run: npm exec ts-node verify-translations-format.ts
diff --git a/gui/scripts/verify-format-specifiers.ts b/gui/scripts/verify-format-specifiers.ts
deleted file mode 100644
index 2d161548d5..0000000000
--- a/gui/scripts/verify-format-specifiers.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import fs from 'fs';
-import { GetTextTranslation, po } from 'gettext-parser';
-import path from 'path';
-
-const LOCALES_DIR = path.join('..', 'locales');
-
-function getLocales(): string[] {
- const localesContent = fs.readdirSync(LOCALES_DIR);
- const localeDirectories = localesContent.filter((item) =>
- fs.statSync(path.join(LOCALES_DIR, item)).isDirectory(),
- );
- return localeDirectories;
-}
-
-function parseTranslationsForLocale(locale: string): GetTextTranslation[] {
- const poFileContents = fs.readFileSync(path.join(LOCALES_DIR, locale, 'messages.po'));
- const contexts = po.parse(poFileContents).translations;
-
- const translations = Object.values(contexts)
- .flatMap((context) => Object.values(context))
- .filter((translation) => translation.msgid !== '');
-
- return translations;
-}
-
-function getFormatSpecifiers(text: string): string[] {
- // Matches both %(name)s and %s.
- return text.match(/%(\(.*?\))?[a-z]/g) ?? [];
-}
-
-function formatSpecifiersEquals(source: string[], translation: string[]): boolean {
- const sortedTranslation = translation.sort();
- return (
- source.length === translation.length &&
- source.sort().every((value, index) => value === sortedTranslation[index])
- );
-}
-
-function checkTranslationImpl(msgid: string, msgstr: string): boolean {
- const sourceFormatSpecifiers = getFormatSpecifiers(msgid);
- const translationFormatSpecifiers = getFormatSpecifiers(msgstr);
- return formatSpecifiersEquals(sourceFormatSpecifiers, translationFormatSpecifiers);
-}
-
-function checkTranslation(translation: GetTextTranslation): boolean {
- return translation.msgstr
- .map((msgstr) => {
- // Make sure that the translation matches either the singular or plural.
- const equal =
- checkTranslationImpl(translation.msgid, msgstr) ||
- (translation.msgid_plural && checkTranslationImpl(translation.msgid_plural, msgstr));
-
- if (!equal) {
- console.error(`Error in "${translation.msgid}", "${msgstr}"`);
- }
-
- return equal;
- })
- .every((result) => result);
-}
-
-const isCorrect = getLocales()
- .map(parseTranslationsForLocale)
- // Map first to output all errors
- .map((translations) => translations.every(checkTranslation))
- .every((result) => result);
-
-if (isCorrect) {
- console.log('Looks good!');
-} else {
- console.error('See above errors');
- process.exit(1);
-}
diff --git a/gui/scripts/verify-translations-format.ts b/gui/scripts/verify-translations-format.ts
new file mode 100644
index 0000000000..2ed927feaa
--- /dev/null
+++ b/gui/scripts/verify-translations-format.ts
@@ -0,0 +1,130 @@
+import fs from 'fs';
+import { GetTextTranslation, po } from 'gettext-parser';
+import path from 'path';
+
+const LOCALES_DIR = path.join('..', 'locales');
+
+const ALLOWED_TAGS = ['b'];
+
+function getLocales(): string[] {
+ const localesContent = fs.readdirSync(LOCALES_DIR);
+ const localeDirectories = localesContent.filter((item) =>
+ fs.statSync(path.join(LOCALES_DIR, item)).isDirectory(),
+ );
+ return localeDirectories;
+}
+
+function parseTranslationsForLocale(locale: string): GetTextTranslation[] {
+ const poFileContents = fs.readFileSync(path.join(LOCALES_DIR, locale, 'messages.po'));
+ const contexts = po.parse(poFileContents).translations;
+
+ const translations = Object.values(contexts)
+ .flatMap((context) => Object.values(context))
+ .filter((translation) => translation.msgid !== '');
+
+ return translations;
+}
+
+function getFormatSpecifiers(text: string): string[] {
+ // Matches both %(name)s and %s.
+ return text.match(/%(\(.*?\))?[a-z]/g) ?? [];
+}
+
+function formatSpecifiersEquals(source: string[], translation: string[]): boolean {
+ const sortedTranslation = translation.sort();
+ return (
+ source.length === translation.length &&
+ source.sort().every((value, index) => value === sortedTranslation[index])
+ );
+}
+
+function checkFormatSpecifiersImpl(msgid: string, msgstr: string): boolean {
+ const sourceFormatSpecifiers = getFormatSpecifiers(msgid);
+ const translationFormatSpecifiers = getFormatSpecifiers(msgstr);
+ return formatSpecifiersEquals(sourceFormatSpecifiers, translationFormatSpecifiers);
+}
+
+function checkFormatSpecifiers(translation: GetTextTranslation): boolean {
+ return translation.msgstr
+ .map((msgstr) => {
+ // Make sure that the translation matches either the singular or plural.
+ const equal =
+ checkFormatSpecifiersImpl(translation.msgid, msgstr) ||
+ (translation.msgid_plural && checkFormatSpecifiersImpl(translation.msgid_plural, msgstr));
+
+ if (!equal) {
+ console.error(`Error in "${translation.msgid}", "${msgstr}"`);
+ }
+
+ return equal;
+ })
+ .every((result) => result);
+}
+
+function checkHtmlTagsImpl(value: string): { correct: boolean, amount: number } {
+ const tagsRegexp = new RegExp("<.*?>", "g");
+ const tags = value.match(tagsRegexp) ?? [];
+ const tagTypes = tags.map((tag) => tag.slice(1, -1));
+
+ // Make sure tags match by pushing start-tags to a stack and matching closing tags with the last
+ // item.
+ let tagStack: string[] = [];
+ for (let tag of tagTypes) {
+ const endTag = tag.startsWith('/');
+ tag = endTag ? tag.slice(1) : tag;
+
+ if (!ALLOWED_TAGS.includes(tag)) {
+ console.error(`Tag "<${tag}>" not allowed: "${value}"`);
+ return { correct: false, amount: NaN };
+ }
+
+ if (endTag) {
+ // End tags require a matching start tag.
+ if (tag !== tagStack.pop()) {
+ console.error(`Closing non-existent start-tag (</${tag}>) in "${value}"`);
+ return { correct: false, amount: NaN };
+ }
+ } else {
+ tagStack.push(tag);
+ }
+ }
+
+ if (tagStack.length > 0) {
+ console.error(`Missing closing-tags (${tagStack}) in "${value}"`);
+ return { correct: false, amount: NaN };
+ }
+
+ return { correct: true, amount: tags.length / 2 };
+}
+
+function checkHtmlTags(translation: GetTextTranslation): boolean {
+ let { correct, amount: sourceAmount } = checkHtmlTagsImpl(translation.msgid);
+
+ let translationsCorrect = translation.msgstr.every((value) => {
+ let { correct, amount } = checkHtmlTagsImpl(value);
+ // The amount doesn't make sense if the string isn't correctly formatted.
+ if (correct && amount !== sourceAmount) {
+ console.error(`Incorrect amount of tags in translation for "${translation.msgid}": "${value}"`);
+ }
+ return correct && amount === sourceAmount;
+ });
+
+ return correct && translationsCorrect;
+}
+
+function checkTranslation(translation: GetTextTranslation): boolean {
+ return checkFormatSpecifiers(translation) && checkHtmlTags(translation);
+}
+
+const isCorrect = getLocales()
+ .map(parseTranslationsForLocale)
+ // Map first to output all errors
+ .map((translations) => translations.every(checkTranslation))
+ .every((result) => result);
+
+if (isCorrect) {
+ console.log('Looks good!');
+} else {
+ console.error('See above errors');
+ process.exit(1);
+}