Require languages to be at least 75% translated

This commit is contained in:
schroda
2025-02-17 02:29:50 +01:00
parent 5dcbf6c168
commit 0a34ae01ba
6 changed files with 123 additions and 37 deletions

View File

@@ -19,30 +19,7 @@ import HttpBackend from 'i18next-http-backend';
*
* This is generated by the `i18n:gen-resources` script
*/
export const i18nResources = [
'ar',
'bn',
'da',
'de',
'en',
'es',
'fr',
'id',
'it',
'ja',
'ko',
'nb-NO',
'nl',
'pt',
'ru',
'sv',
'th',
'tr',
'uk',
'vi',
'zh_Hans',
'zh_Hant',
] as const;
export const i18nResources = ['en', 'es', 'pt', 'ru', 'tr', 'vi', 'zh_Hans', 'zh_Hant'] as const;
export type I18nResourceCode = (typeof i18nResources)[number];

View File

@@ -8,21 +8,55 @@
import path from 'path';
import fs from 'fs';
import {
fetchWeblateLanguageStats,
getWeblateLanguageStatsFor,
meetsTranslatedPercentThreshold as meetsTranslatedPercentThresholdBase,
} from './weblate/Weblate.utils.ts';
import { WeblateLanguageStatistic } from './weblate/Weblate.types.ts';
import { TRANSLATED_PERCENT_THRESHOLD } from './weblate/Weblate.constants.ts';
const outputFilePath = path.join(import.meta.dirname, '../../src/i18n/index.ts');
const resourcesDirPath = path.join(import.meta.dirname, '../../public/locales');
const resourceFileNames = fs.readdirSync(resourcesDirPath);
const extractLanguageCode = (resourceFileNames: string): string =>
path.basename(resourceFileNames, path.extname(resourceFileNames));
const resourceNames = resourceFileNames.map((resourceFileName) =>
path.basename(resourceFileName, path.extname(resourceFileName)),
);
const meetsTranslatedPercentThreshold = (
resourceFileName: string,
weblateLanguageStats: WeblateLanguageStatistic[],
): boolean => {
const code = extractLanguageCode(resourceFileName);
const { name, translated_percent: translatedPercent } = getWeblateLanguageStatsFor(code, weblateLanguageStats);
const outputFileContent = fs.readFileSync(outputFilePath, 'utf-8');
const meetsThreshold = meetsTranslatedPercentThresholdBase(translatedPercent);
const updatedFileContent = outputFileContent.replace(
if (!meetsThreshold) {
console.log(
`Language "${name} (${code})" does not meet the required translation percentage threshold (${translatedPercent} < ${TRANSLATED_PERCENT_THRESHOLD})`,
);
}
return meetsThreshold;
};
const generateResources = async () => {
const weblateLanguageStats = await fetchWeblateLanguageStats();
const resourceFileNames = fs.readdirSync(resourcesDirPath);
const resourceNames = resourceFileNames
.filter((resourceFileName) => meetsTranslatedPercentThreshold(resourceFileName, weblateLanguageStats))
.map(extractLanguageCode);
const outputFileContent = fs.readFileSync(outputFilePath, 'utf-8');
const updatedFileContent = outputFileContent.replace(
/(export const i18nResources = \[)[\s\S]*?(])/g,
`$1'${resourceNames.join("', '")}'$2`,
);
);
fs.writeFileSync(outputFilePath, updatedFileContent);
fs.writeFileSync(outputFilePath, updatedFileContent);
};
generateResources();

View File

@@ -18,9 +18,22 @@ import {
UsernameByUserUrl,
UserUrlsByTranslationUrl,
WeblateChangeResult,
WeblateLanguageStatistic,
} from '../weblate/Weblate.types.ts';
import { fetchWeblateChanges, getLanguageName, getUsername, validateWeblateDates } from '../weblate/Weblate.utils.ts';
import { creditRelevantActions, TRANSLATION_CHANGELOG_YARG_OPTIONS_DEFAULT } from '../weblate/Weblate.constants.ts';
import {
fetchWeblateChanges,
fetchWeblateLanguageStats,
getLanguageName,
getUsername,
getWeblateLanguageStatsFor,
meetsTranslatedPercentThreshold,
validateWeblateDates,
} from '../weblate/Weblate.utils.ts';
import {
creditRelevantActions,
TRANSLATED_PERCENT_THRESHOLD,
TRANSLATION_CHANGELOG_YARG_OPTIONS_DEFAULT,
} from '../weblate/Weblate.constants.ts';
const extractContributionInfoFromChanges = (
changes: WeblateChangeResult[],
@@ -105,11 +118,34 @@ const getUserContributionByLanguage = (
}),
);
const doesLanguageOfChangeMeetTranslatePercentThreshold = (
change: WeblateChangeResult,
stats: WeblateLanguageStatistic[],
): boolean => {
// format: 'https://hosted.weblate.org/api/translations/suwayomi/suwayomi-webui/ar/'
const code = change.translation.split('/').slice(-2)[0];
const langaugeStats = getWeblateLanguageStatsFor(code, stats);
return meetsTranslatedPercentThreshold(langaugeStats.translated_percent);
};
const getContributorsForRange = async (url: string, authToken: string): Promise<ContributionsByLanguage> => {
const weblateChanges = await fetchWeblateChanges(url, authToken);
const weblateLanguageStats = await fetchWeblateLanguageStats();
const validWeblateChanges = weblateChanges.filter((change) =>
doesLanguageOfChangeMeetTranslatePercentThreshold(change, weblateLanguageStats),
);
const invalidWeblateChangesAmount = Math.abs(validWeblateChanges.length - weblateChanges.length);
if (invalidWeblateChangesAmount > 0) {
console.log(
`Filtered out ${invalidWeblateChangesAmount} invalid change(s) due languages not meeting the translated percent threshold (${TRANSLATED_PERCENT_THRESHOLD})`,
);
}
const { userUrlsByTranslationUrl, actionsByTranslationUrlByUserUrl } =
extractContributionInfoFromChanges(weblateChanges);
extractContributionInfoFromChanges(validWeblateChanges);
const usernameByUserUrl = await getUsernameByUserUrlMap(userUrlsByTranslationUrl, authToken);
const languageNameByTranslationUrl = await getLanguageNameByTranslationUrl(userUrlsByTranslationUrl, authToken);

View File

@@ -21,3 +21,5 @@ export const TRANSLATION_CHANGELOG_YARG_OPTIONS_DEFAULT = {
requiredContributionCount: 10,
keepKnownContributors: true,
};
export const TRANSLATED_PERCENT_THRESHOLD = 75;

View File

@@ -55,7 +55,9 @@ export interface WeblateUserPayload {
export interface WeblateLanguagePayload {
language: {
name: string;
code: string;
};
translated_percent: number;
}
export type Username = string;
@@ -78,3 +80,9 @@ export interface Contributor {
}
export type ContributionsByLanguage = Record<LanguageName, Contributor[]>;
export interface WeblateLanguageStatistic {
translated_percent: number;
code: string;
name: string;
}

View File

@@ -10,8 +10,11 @@ import {
WeblateChangePayload,
WeblateChangeResult,
WeblateLanguagePayload,
WeblateLanguageStatistic,
WeblateUserPayload,
} from '@/../tools/scripts/weblate/Weblate.types.ts';
import tokens from '../tokens.json';
import { TRANSLATED_PERCENT_THRESHOLD } from '@/../tools/scripts/weblate/Weblate.constants.ts';
export const fetchData = async <T = any>(url: string, authToken: string): Promise<T> => {
const response = await fetch(url, {
@@ -56,6 +59,12 @@ export const getLanguageName = async (url: string, authToken: string): Promise<s
return languagePayload.language.name.replace(/\(([a-zA-Z]+) Han script\)/g, '($1)');
};
export const fetchWeblateLanguageStats = async () =>
fetchData(
'https://hosted.weblate.org/api/components/suwayomi/suwayomi-webui/statistics/?format=json-flat',
tokens.weblateToken,
);
const dateRegex = /[0-9]{4}-[0-9]{2}-[0-9]{2}/g;
const isValidIS08601Date = (date: string): boolean => !!date.match(dateRegex);
export const validateWeblateDates = (afterDate: string, beforeDate: string) => {
@@ -65,3 +74,23 @@ export const validateWeblateDates = (afterDate: string, beforeDate: string) => {
);
}
};
export const normalizeCode = (code: string): string => code.replace(/[-_]/g, '');
export const getWeblateLanguageStatsFor = (
code: string,
stats: WeblateLanguageStatistic[],
): WeblateLanguageStatistic => {
const langaugeStats = stats.find((stat) => normalizeCode(stat.code) === normalizeCode(code));
if (!langaugeStats) {
throw new Error(`Weblate language stats for "${code} do not exist`);
}
return langaugeStats;
};
export const meetsTranslatedPercentThreshold = (
translatedPercent: number,
threshold: number = TRANSLATED_PERCENT_THRESHOLD,
): boolean => translatedPercent >= threshold;