mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 06:57:35 +08:00
add translation process badge
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const FRONTEND_LOCALES_DIR = path.join(__dirname, '..', '..', 'src', 'locales');
|
||||
const BACKEND_LOCALES_DIR = path.join(__dirname, '..', '..', 'pkg', 'locales');
|
||||
const OUTPUT_DIR = process.argv[2] || path.join(__dirname, '..', '..', 'i18n-badge');
|
||||
|
||||
const DEFAULT_LANGUAGE_TAG = 'en';
|
||||
|
||||
const BACKEND_SKIP_STRUCTS = new Set([
|
||||
'GlobalTextItems',
|
||||
'DefaultTypes',
|
||||
'DataConverterTextItems',
|
||||
]);
|
||||
|
||||
function discoverFrontendLanguages() {
|
||||
const indexPath = path.join(FRONTEND_LOCALES_DIR, 'index.ts');
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
|
||||
const importMap = {};
|
||||
const importRegex = /import\s+(\w+)\s+from\s+['"]\.\/([\w_]+\.json)['"]/g;
|
||||
let match;
|
||||
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
importMap[match[1]] = match[2];
|
||||
}
|
||||
|
||||
const result = {};
|
||||
const langRegex = /['"]([^'"]+)['"]\s*:\s*\{[^}]*content\s*:\s*(\w+)/g;
|
||||
|
||||
while ((match = langRegex.exec(content)) !== null) {
|
||||
const tag = match[1];
|
||||
const varName = match[2];
|
||||
|
||||
if (importMap[varName]) {
|
||||
result[tag] = importMap[varName];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function discoverBackendLanguages() {
|
||||
const allLocalesPath = path.join(BACKEND_LOCALES_DIR, 'all_locales.go');
|
||||
const content = fs.readFileSync(allLocalesPath, 'utf-8');
|
||||
|
||||
const result = {};
|
||||
const entryRegex = /"([^"]+)"\s*:\s*\{[^}]*Content\s*:\s*(\w+)/g;
|
||||
let match;
|
||||
|
||||
while ((match = entryRegex.exec(content)) !== null) {
|
||||
const tag = match[1];
|
||||
const fileName = tag.toLowerCase().replace(/-/g, '_') + '.go';
|
||||
const filePath = path.join(BACKEND_LOCALES_DIR, fileName);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
result[tag] = fileName;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function flattenJSON(obj, prefix) {
|
||||
const result = {};
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const fullKey = prefix ? prefix + '.' + key : key;
|
||||
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
Object.assign(result, flattenJSON(obj[key], fullKey));
|
||||
} else {
|
||||
result[fullKey] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function shouldSkipFrontendKey(key) {
|
||||
if (key.startsWith('global.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('default.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('format.')) {
|
||||
if (key.startsWith('format.misc.')) {
|
||||
if (key === 'format.misc.multiTextJoinSeparator') {
|
||||
return true;
|
||||
} else if (key === 'format.misc.eachMonthDayInMonthDays') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else if (key.startsWith('datetime.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('timezone.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('currency.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('mapprovider.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('parameter.id')) {
|
||||
return true;
|
||||
} else if (key.startsWith('encoding.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('document.anchor.')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractGoStringFields(content) {
|
||||
const fields = [];
|
||||
const structBlockRegex = /(\w+):\s*&\w+\{([^}]*)\}/gs;
|
||||
let blockMatch;
|
||||
|
||||
while ((blockMatch = structBlockRegex.exec(content)) !== null) {
|
||||
const structName = blockMatch[1];
|
||||
const blockBody = blockMatch[2];
|
||||
const fieldRegex = /(\w+):\s+"((?:[^"\\]|\\.)*)"/g;
|
||||
let fieldMatch;
|
||||
|
||||
while ((fieldMatch = fieldRegex.exec(blockBody)) !== null) {
|
||||
fields.push({
|
||||
struct: structName,
|
||||
name: fieldMatch[1],
|
||||
value: fieldMatch[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getProgressColor(progress) {
|
||||
if (progress >= 95) {
|
||||
return 'brightgreen';
|
||||
} else if (progress >= 90) {
|
||||
return 'green';
|
||||
} else if (progress >= 70) {
|
||||
return 'yellowgreen';
|
||||
} else if (progress >= 50) {
|
||||
return 'yellow';
|
||||
} else if (progress >= 20) {
|
||||
return 'orange';
|
||||
} else {
|
||||
return 'red';
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const frontendLangs = discoverFrontendLanguages();
|
||||
const backendLangs = discoverBackendLanguages();
|
||||
const allTags = new Set([...Object.keys(frontendLangs), ...Object.keys(backendLangs)]);
|
||||
|
||||
console.log('Discovered ' + allTags.size + ' languages: ' + [...allTags].sort().join(', '));
|
||||
|
||||
const defaultFrontendJSON = JSON.parse(fs.readFileSync(path.join(FRONTEND_LOCALES_DIR, `${DEFAULT_LANGUAGE_TAG}.json`), 'utf-8'));
|
||||
const defaultFrontendItemsMap = flattenJSON(defaultFrontendJSON, '');
|
||||
const defaultFrontendKeys = Object.keys(defaultFrontendItemsMap);
|
||||
const frontendTranslatableKeys = defaultFrontendKeys.filter(function (k) {
|
||||
return !shouldSkipFrontendKey(k);
|
||||
});
|
||||
const frontendSkippedCount = defaultFrontendKeys.length - frontendTranslatableKeys.length;
|
||||
const frontendTotal = frontendTranslatableKeys.length;
|
||||
|
||||
const defaultBackendContent = fs.readFileSync(path.join(BACKEND_LOCALES_DIR, `${DEFAULT_LANGUAGE_TAG}.go`), 'utf-8');
|
||||
const defaultBackendItems = extractGoStringFields(defaultBackendContent);
|
||||
const defaultBackendTranslatableItems = defaultBackendItems.filter(function (f) {
|
||||
return !BACKEND_SKIP_STRUCTS.has(f.struct);
|
||||
});
|
||||
const backendSkippedCount = defaultBackendItems.length - defaultBackendTranslatableItems.length;
|
||||
const backendTotal = defaultBackendTranslatableItems.length;
|
||||
|
||||
console.log('Frontend: ' + frontendTotal + ' translatable keys (' + frontendSkippedCount + ' excluded)');
|
||||
console.log('Backend: ' + backendTotal + ' translatable fields (' + backendSkippedCount + ' excluded)');
|
||||
|
||||
const results = {};
|
||||
const untranslatedKeys = {};
|
||||
|
||||
for (const tag of allTags) {
|
||||
results[tag] = {
|
||||
languageTag: tag,
|
||||
frontendTranslated: 0,
|
||||
frontendTotal: frontendTotal,
|
||||
backendTranslated: 0,
|
||||
backendTotal: backendTotal
|
||||
};
|
||||
untranslatedKeys[tag] = [];
|
||||
}
|
||||
|
||||
for (const tag of Object.keys(frontendLangs)) {
|
||||
if (tag === DEFAULT_LANGUAGE_TAG) {
|
||||
results[tag].frontendTranslated = frontendTotal;
|
||||
continue;
|
||||
}
|
||||
|
||||
const file = frontendLangs[tag];
|
||||
const filePath = path.join(FRONTEND_LOCALES_DIR, file);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
const kv = flattenJSON(json, '');
|
||||
let translated = 0;
|
||||
|
||||
for (const key of frontendTranslatableKeys) {
|
||||
if (kv[key] !== undefined && kv[key] !== '' && kv[key] !== defaultFrontendItemsMap[key]) {
|
||||
translated++;
|
||||
} else {
|
||||
untranslatedKeys[tag].push({ source: 'frontend', key: key, defaultValue: defaultFrontendItemsMap[key], value: kv[key] });
|
||||
}
|
||||
}
|
||||
|
||||
results[tag].frontendTranslated = translated;
|
||||
}
|
||||
|
||||
for (const tag of Object.keys(backendLangs)) {
|
||||
if (tag === DEFAULT_LANGUAGE_TAG) {
|
||||
results[tag].backendTranslated = backendTotal;
|
||||
continue;
|
||||
}
|
||||
|
||||
const file = backendLangs[tag];
|
||||
const filePath = path.join(BACKEND_LOCALES_DIR, file);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const fields = extractGoStringFields(content).filter(function (f) {
|
||||
return !BACKEND_SKIP_STRUCTS.has(f.struct);
|
||||
});
|
||||
let translated = 0;
|
||||
|
||||
for (let i = 0; i < defaultBackendTranslatableItems.length; i++) {
|
||||
if (i < fields.length && fields[i].value !== defaultBackendTranslatableItems[i].value) {
|
||||
translated++;
|
||||
} else {
|
||||
untranslatedKeys[tag].push({ source: 'backend', key: defaultBackendTranslatableItems[i].struct + '.' + defaultBackendTranslatableItems[i].name, defaultValue: defaultBackendTranslatableItems[i].value, value: (i < fields.length) ? fields[i].value : null });
|
||||
}
|
||||
}
|
||||
|
||||
results[tag].backendTranslated = translated;
|
||||
}
|
||||
|
||||
for (const tag of Object.keys(results)) {
|
||||
const r = results[tag];
|
||||
const totalTranslated = r.frontendTranslated + r.backendTranslated;
|
||||
const totalItems = r.frontendTotal + r.backendTotal;
|
||||
r.totalProgress = Math.round((totalTranslated / totalItems) * 10000) / 100;
|
||||
}
|
||||
|
||||
const sortedResults = {};
|
||||
var sortedTags = Object.keys(results).sort();
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
sortedResults[tag] = results[tag];
|
||||
}
|
||||
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
var badgesDir = path.join(OUTPUT_DIR, 'badges');
|
||||
|
||||
if (!fs.existsSync(badgesDir)) {
|
||||
fs.mkdirSync(badgesDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, 'i18n-progress.json'),
|
||||
JSON.stringify(sortedResults, null, 4) + '\n'
|
||||
);
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
const data = sortedResults[tag];
|
||||
const badge = {
|
||||
schemaVersion: 1,
|
||||
label: 'translation',
|
||||
message: data.totalProgress + '%',
|
||||
color: getProgressColor(data.totalProgress)
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(badgesDir, tag + '.json'),
|
||||
JSON.stringify(badge, null, 4) + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
var untranslatedDir = path.join(OUTPUT_DIR, 'untranslated');
|
||||
|
||||
if (!fs.existsSync(untranslatedDir)) {
|
||||
fs.mkdirSync(untranslatedDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
const items = untranslatedKeys[tag] || [];
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(untranslatedDir, tag + '.json'),
|
||||
JSON.stringify(items, null, 4) + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
const data = sortedResults[tag];
|
||||
const missingCount = (untranslatedKeys[tag] || []).length;
|
||||
console.log(tag + ': ' + data.totalProgress + '% (frontend: ' + data.frontendTranslated + '/' + data.frontendTotal + ', backend: ' + data.backendTranslated + '/' + data.backendTotal + ', untranslated: ' + missingCount + ')');
|
||||
}
|
||||
|
||||
console.log('\nResults written to ' + OUTPUT_DIR);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,73 @@
|
||||
name: Update i18n Translation Progress Badges
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'src/locales/**'
|
||||
- 'pkg/locales/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-i18n-progress:
|
||||
if: vars.UPDATE_I18N_BADGE_REPO == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Update translation progress data
|
||||
run: |
|
||||
node .github/scripts/update-i18n-progress.js ${{ runner.temp }}/i18n-badge
|
||||
|
||||
- name: Checkout badge repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: mayswind/ezbookkeeping-i18n-badge
|
||||
token: ${{ secrets.I18N_BADGE_REPO_TOKEN }}
|
||||
path: ezbookkeeping-i18n-badge
|
||||
|
||||
- name: Update badge data
|
||||
run: |
|
||||
cp ${{ runner.temp }}/i18n-badge/i18n-progress.json ezbookkeeping-i18n-badge/
|
||||
mkdir -p ezbookkeeping-i18n-badge/badges
|
||||
cp ${{ runner.temp }}/i18n-badge/badges/*.json ezbookkeeping-i18n-badge/badges/
|
||||
mkdir -p ezbookkeeping-i18n-badge/untranslated
|
||||
cp ${{ runner.temp }}/i18n-badge/untranslated/*.json ezbookkeeping-i18n-badge/untranslated/
|
||||
|
||||
- name: Commit and push
|
||||
run: |
|
||||
cd ezbookkeeping-i18n-badge
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "Update i18n progress data (${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})"
|
||||
git push
|
||||
fi
|
||||
|
||||
- name: Purge GitHub camo image cache
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
CAMO_URLS=$(curl -s -H "Accept: application/vnd.github.html+json" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/${{ github.repository }}/readme" | grep -oP 'https://camo\.githubusercontent\.com/[^"]+' | sort -u)
|
||||
|
||||
if [ -z "$CAMO_URLS" ]; then
|
||||
echo "No camo URLs found, skipping cache purge"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for url in $CAMO_URLS; do
|
||||
echo "Purging: $url"
|
||||
curl -s -X PURGE "$url" > /dev/null
|
||||
done
|
||||
|
||||
echo "Purged $(echo "$CAMO_URLS" | wc -l) camo URLs"
|
||||
@@ -129,27 +129,27 @@ Help make ezBookkeeping accessible to users around the world. We welcome help to
|
||||
|
||||
Currently available translations:
|
||||
|
||||
| Tag | Language | Contributors |
|
||||
| --- | --- | --- |
|
||||
| de | Deutsch | [@chrgm](https://github.com/chrgm), [@1270o1](https://github.com/1270o1) |
|
||||
| en | English | / |
|
||||
| es | Español | [@Miguelonlonlon](https://github.com/Miguelonlonlon), [@abrugues](https://github.com/abrugues), [@AndresTeller](https://github.com/AndresTeller), [@diegofercri](https://github.com/diegofercri) |
|
||||
| fr | Français | [@brieucdlf](https://github.com/brieucdlf) |
|
||||
| it | Italiano | [@waron97](https://github.com/waron97) |
|
||||
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) |
|
||||
| kn | ಕನ್ನಡ | [@Darshanbm05](https://github.com/Darshanbm05) |
|
||||
| ko | 한국어 | [@overworks](https://github.com/overworks) |
|
||||
| nl | Nederlands | [@automagics](https://github.com/automagics) |
|
||||
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus), [@balaios](https://github.com/balaios) |
|
||||
| ru | Русский | [@artegoser](https://github.com/artegoser), [@dshemin](https://github.com/dshemin) |
|
||||
| sl | Slovenščina | [@thehijacker](https://github.com/thehijacker) |
|
||||
| ta | தமிழ் | [@hhharsha36](https://github.com/hhharsha36) |
|
||||
| th | ไทย | [@natthavat28](https://github.com/natthavat28) |
|
||||
| tr | Türkçe | [@aydnykn](https://github.com/aydnykn) |
|
||||
| uk | Українська | [@nktlitvinenko](https://github.com/nktlitvinenko) |
|
||||
| vi | Tiếng Việt | [@f97](https://github.com/f97) |
|
||||
| zh-Hans | 中文 (简体) | / |
|
||||
| zh-Hant | 中文 (繁體) | / |
|
||||
| Tag | Language | Progress | Contributors |
|
||||
| --- | --- | --- | --- |
|
||||
| de | Deutsch |  | [@chrgm](https://github.com/chrgm), [@1270o1](https://github.com/1270o1) |
|
||||
| en | English |  | / |
|
||||
| es | Español |  | [@Miguelonlonlon](https://github.com/Miguelonlonlon), [@abrugues](https://github.com/abrugues), [@AndresTeller](https://github.com/AndresTeller), [@diegofercri](https://github.com/diegofercri) |
|
||||
| fr | Français |  | [@brieucdlf](https://github.com/brieucdlf) |
|
||||
| it | Italiano |  | [@waron97](https://github.com/waron97) |
|
||||
| ja | 日本語 |  | [@tkymmm](https://github.com/tkymmm) |
|
||||
| kn | ಕನ್ನಡ |  | [@Darshanbm05](https://github.com/Darshanbm05) |
|
||||
| ko | 한국어 |  | [@overworks](https://github.com/overworks) |
|
||||
| nl | Nederlands |  | [@automagics](https://github.com/automagics) |
|
||||
| pt-BR | Português (Brasil) |  | [@thecodergus](https://github.com/thecodergus), [@balaios](https://github.com/balaios) |
|
||||
| ru | Русский |  | [@artegoser](https://github.com/artegoser), [@dshemin](https://github.com/dshemin) |
|
||||
| sl | Slovenščina |  | [@thehijacker](https://github.com/thehijacker) |
|
||||
| ta | தமிழ் |  | [@hhharsha36](https://github.com/hhharsha36) |
|
||||
| th | ไทย |  | [@natthavat28](https://github.com/natthavat28) |
|
||||
| tr | Türkçe |  | [@aydnykn](https://github.com/aydnykn) |
|
||||
| uk | Українська |  | [@nktlitvinenko](https://github.com/nktlitvinenko) |
|
||||
| vi | Tiếng Việt |  | [@f97](https://github.com/f97) |
|
||||
| zh-Hans | 中文 (简体) |  | / |
|
||||
| zh-Hant | 中文 (繁體) |  | / |
|
||||
|
||||
## Documentation
|
||||
1. [English](https://ezbookkeeping.mayswind.net)
|
||||
|
||||
Reference in New Issue
Block a user