feat: localize official plugins in Russian
This commit is contained in:
+9
-2
@@ -79,7 +79,14 @@ with open(path, 'w', encoding='utf-8') as f:
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. backend binary
|
||||
# 3. locale catalogs declared by plugin.json
|
||||
if [ -d "$plugin_dir/locales" ]; then
|
||||
mkdir -p "$dist_dir/locales"
|
||||
cp -r "$plugin_dir/locales/." "$dist_dir/locales/"
|
||||
echo " └─ locales ($(find "$dist_dir/locales" -type f | wc -l) file(s))"
|
||||
fi
|
||||
|
||||
# 4. backend binary
|
||||
if [ -d "$plugin_dir/backend" ]; then
|
||||
# Find the compiled binary (same name as plugin directory)
|
||||
local bin_name="$plugin_name"
|
||||
@@ -91,7 +98,7 @@ with open(path, 'w', encoding='utf-8') as f:
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. Verify dist package has at least plugin.json
|
||||
# 5. Verify dist package has at least plugin.json
|
||||
if [ ! -f "$dist_dir/plugin.json" ]; then
|
||||
echo " ❌ dist package missing plugin.json"
|
||||
return 1
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const pluginsRoot = path.join(root, 'plugins');
|
||||
const contributionFields = {
|
||||
views: 'title',
|
||||
commands: 'title',
|
||||
settingsPanels: 'title',
|
||||
sidebarItems: 'title',
|
||||
fileActions: 'label',
|
||||
noteActions: 'label',
|
||||
contextMenuEntries: 'label',
|
||||
searchProviders: 'label',
|
||||
statusBarItems: 'label',
|
||||
openProviders: 'title',
|
||||
workspaceItems: 'title',
|
||||
};
|
||||
const problems = [];
|
||||
|
||||
for (const entry of fs.readdirSync(pluginsRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const pluginRoot = path.join(pluginsRoot, entry.name);
|
||||
const manifestPath = path.join(pluginRoot, 'plugin.json');
|
||||
if (!fs.existsSync(manifestPath)) continue;
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const localization = manifest.localization;
|
||||
if (!localization || localization.defaultLocale !== 'en') {
|
||||
problems.push(`${manifest.id}: localization.defaultLocale must be en`);
|
||||
continue;
|
||||
}
|
||||
if (!localization.locales || !localization.locales.en || !localization.locales.ru) {
|
||||
problems.push(`${manifest.id}: en and ru catalogs must be declared`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const catalogs = {};
|
||||
for (const locale of ['en', 'ru']) {
|
||||
const relative = localization.locales[locale];
|
||||
const catalogPath = path.resolve(pluginRoot, relative);
|
||||
if (!catalogPath.startsWith(pluginRoot + path.sep) || !fs.existsSync(catalogPath)) {
|
||||
problems.push(`${manifest.id}: missing safe ${locale} catalog at ${relative}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
catalogs[locale] = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||||
} catch (error) {
|
||||
problems.push(`${manifest.id}: invalid ${locale} catalog: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
for (const [key, value] of Object.entries(catalogs[locale])) {
|
||||
if (typeof value !== 'string') problems.push(`${manifest.id}: ${locale}.${key} must be a string`);
|
||||
}
|
||||
}
|
||||
if (!catalogs.en || !catalogs.ru) continue;
|
||||
|
||||
const enKeys = Object.keys(catalogs.en).sort();
|
||||
const ruKeys = Object.keys(catalogs.ru).sort();
|
||||
if (JSON.stringify(enKeys) !== JSON.stringify(ruKeys)) {
|
||||
problems.push(`${manifest.id}: en/ru catalog keys differ`);
|
||||
}
|
||||
|
||||
const required = ['manifest.name'];
|
||||
if (manifest.description) required.push('manifest.description');
|
||||
for (const [point, field] of Object.entries(contributionFields)) {
|
||||
for (const item of manifest.contributes?.[point] || []) {
|
||||
required.push(`contributions.${point}.${item.id}.${field}`);
|
||||
}
|
||||
}
|
||||
for (const key of required) {
|
||||
if (!Object.prototype.hasOwnProperty.call(catalogs.en, key)) problems.push(`${manifest.id}: en catalog missing ${key}`);
|
||||
if (!Object.prototype.hasOwnProperty.call(catalogs.ru, key)) problems.push(`${manifest.id}: ru catalog missing ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (problems.length > 0) {
|
||||
problems.forEach((problem) => console.error(` FAIL ${problem}`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(' OK official plugin locale catalogs are complete and aligned');
|
||||
@@ -76,6 +76,19 @@ else
|
||||
echo " ℹ️ python3 not available — skipping manifest validation"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
# Verify plugin-owned English and Russian catalogs.
|
||||
echo "[localization catalogs]"
|
||||
if command -v node &>/dev/null; then
|
||||
set +e
|
||||
node "$ROOT/scripts/check-locales.mjs"
|
||||
STATUS=$?
|
||||
set -e
|
||||
report "localization catalogs" "$STATUS"
|
||||
else
|
||||
echo " ⚠️ node not available — skipping localization catalog validation"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
# Guard official plugins against bypassing the v2 plugin API for note features.
|
||||
echo "[frontend API boundary]"
|
||||
|
||||
@@ -8,6 +8,10 @@ const sourcePath = path.join(root, 'plugins', 'todo', 'frontend', 'src', 'index.
|
||||
const manifestPath = path.join(root, 'plugins', 'todo', 'plugin.json');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const localeCatalogs = {
|
||||
en: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'todo', 'locales', 'en.json'), 'utf8')),
|
||||
ru: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'todo', 'locales', 'ru.json'), 'utf8')),
|
||||
};
|
||||
|
||||
class FakeNode {
|
||||
constructor(tagName) {
|
||||
@@ -140,8 +144,10 @@ function loadComponent(document, emittedEvents) {
|
||||
return component;
|
||||
}
|
||||
|
||||
function makeApi(initialSettings = {}) {
|
||||
function makeApi(initialSettings = {}, initialLocale = 'en') {
|
||||
const settings = { ...initialSettings };
|
||||
let locale = initialLocale;
|
||||
const localeListeners = [];
|
||||
return {
|
||||
settings,
|
||||
settingsApi: {
|
||||
@@ -157,8 +163,31 @@ function makeApi(initialSettings = {}) {
|
||||
{ name: 'Project', relativePath: 'Project', type: 'folder' },
|
||||
],
|
||||
},
|
||||
setLocale(nextLocale) {
|
||||
locale = nextLocale;
|
||||
localeListeners.slice().forEach((listener) => listener(locale));
|
||||
},
|
||||
get api() {
|
||||
return { settings: this.settingsApi, files: this.files };
|
||||
return {
|
||||
settings: this.settingsApi,
|
||||
files: this.files,
|
||||
i18n: {
|
||||
getLocale: () => locale,
|
||||
t: (key, params, fallback) => {
|
||||
const message = localeCatalogs[locale]?.[key] || localeCatalogs.en[key] || fallback || key;
|
||||
return message.replace(/\{([^}]+)\}/g, (placeholder, name) => (
|
||||
Object.prototype.hasOwnProperty.call(params || {}, name) ? String(params[name]) : placeholder
|
||||
));
|
||||
},
|
||||
onDidChangeLocale: (listener) => {
|
||||
localeListeners.push(listener);
|
||||
return () => {
|
||||
const index = localeListeners.indexOf(listener);
|
||||
if (index !== -1) localeListeners.splice(index, 1);
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -205,6 +234,13 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
|
||||
if (createdTodo.dueAt !== '2000-01-01' || createdTodo.reminderAt !== '2000-01-01T09:00') throw new Error('Todo due/reminder metadata was not stored');
|
||||
if (!container.textContent.includes('Overdue') || !container.textContent.includes('Reminder due')) throw new Error('due/reminder indicators were not rendered');
|
||||
|
||||
apiState.setLocale('ru');
|
||||
if (!container.textContent.includes('Задачи · Project') || !container.textContent.includes('Просрочено')) {
|
||||
throw new Error('Todo view did not update to Russian without remounting');
|
||||
}
|
||||
if ((apiState.settings['todos:global'] || []).length !== 1) throw new Error('locale change lost Todo state');
|
||||
apiState.setLocale('en');
|
||||
|
||||
byData(container, 'data-todo-action', 'edit').click();
|
||||
byData(container, 'data-todo-input', 'title').value = 'Prepare project review updated';
|
||||
byData(container, 'data-todo-action', 'save').click();
|
||||
|
||||
Reference in New Issue
Block a user