feat: add plugin localization contract
This commit is contained in:
@@ -16,6 +16,9 @@ describe('VerstakPluginAPI contract', () => {
|
||||
expect(typeof api.settings.write).toBe('function');
|
||||
expect(typeof api.storage.data.read).toBe('function');
|
||||
expect(typeof api.storage.data.write).toBe('function');
|
||||
expect(typeof api.i18n.getLocale).toBe('function');
|
||||
expect(typeof api.i18n.t).toBe('function');
|
||||
expect(typeof api.i18n.onDidChangeLocale).toBe('function');
|
||||
expect(typeof api.ui.openSettings).toBe('function');
|
||||
expect(typeof api.capabilities.list).toBe('function');
|
||||
expect(typeof api.commands.register).toBe('function');
|
||||
@@ -58,6 +61,56 @@ describe('VerstakPluginAPI contract', () => {
|
||||
expect(permissionEnum).toContain('workbench.open');
|
||||
});
|
||||
|
||||
test('manifest schema declares safe plugin localization catalogs', () => {
|
||||
const localization = (manifestSchema as any).properties.localization;
|
||||
|
||||
expect(localization.type).toBe('object');
|
||||
expect(localization.required).toEqual(['defaultLocale', 'locales']);
|
||||
expect(localization.properties.defaultLocale.pattern).toBe('^[a-z]{2}(?:-[a-z0-9]+)*$');
|
||||
expect(localization.properties.locales.propertyNames.pattern).toBe('^[a-z]{2}(?:-[a-z0-9]+)*$');
|
||||
expect(localization.properties.locales.additionalProperties.pattern).toBe('^(?![\\\\/])(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$');
|
||||
});
|
||||
|
||||
test('manifest types accept plugin-owned localization catalogs', () => {
|
||||
const manifest: PluginManifest = {
|
||||
schemaVersion: 1,
|
||||
id: 'localized.plugin',
|
||||
name: 'Localized Plugin',
|
||||
version: '0.1.0',
|
||||
apiVersion: '0.1.0',
|
||||
provides: ['localized.example'],
|
||||
permissions: ['ui.register'],
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
locales: {
|
||||
en: 'locales/en.json',
|
||||
ru: 'locales/ru.json',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(manifest.localization?.defaultLocale).toBe('en');
|
||||
expect(manifest.localization?.locales.ru).toBe('locales/ru.json');
|
||||
});
|
||||
|
||||
test('mock i18n translates with fallback and named interpolation', () => {
|
||||
const api = createMockPluginAPI('localized.plugin', {
|
||||
locale: 'ru',
|
||||
messages: {
|
||||
en: { greeting: 'Hello, {name}!', onlyEnglish: 'English fallback' },
|
||||
ru: { greeting: 'Привет, {name}!' },
|
||||
},
|
||||
defaultLocale: 'en',
|
||||
});
|
||||
|
||||
expect(api.i18n.getLocale()).toBe('ru');
|
||||
expect(api.i18n.t('greeting', { name: 'Мир' })).toBe('Привет, Мир!');
|
||||
expect(api.i18n.t('onlyEnglish')).toBe('English fallback');
|
||||
expect(api.i18n.t('missing', undefined, 'Fallback')).toBe('Fallback');
|
||||
expect(api.i18n.t('unknown')).toBe('unknown');
|
||||
expect(typeof api.i18n.onDidChangeLocale(() => {})).toBe('function');
|
||||
});
|
||||
|
||||
test('secrets capability and permissions are declared as dangerous platform contract', () => {
|
||||
const capabilities = ((capabilitiesSchema as any).capabilities || []) as Array<{ name: string; status: string }>;
|
||||
const permissions = ((permissionsSchema as any).permissions || []) as Array<{ name: string; dangerous: boolean }>;
|
||||
|
||||
@@ -23,6 +23,8 @@ import type {
|
||||
|
||||
export type PluginCommandArgs = Record<string, unknown>;
|
||||
export type PluginDataJSON = Record<string, unknown>;
|
||||
export type PluginLocale = 'ru' | 'en';
|
||||
export type TranslationParams = Record<string, string | number>;
|
||||
export type PluginCommandHandler = (
|
||||
args: PluginCommandArgs,
|
||||
declaration: PluginCommandDeclaration
|
||||
@@ -95,6 +97,12 @@ export interface BrowserReceiverPairing {
|
||||
export interface VerstakPluginAPI {
|
||||
readonly pluginId: string;
|
||||
|
||||
i18n: {
|
||||
getLocale(): PluginLocale;
|
||||
t(key: string, params?: TranslationParams, fallback?: string): string;
|
||||
onDidChangeLocale(listener: (locale: PluginLocale) => void): Unsubscribe;
|
||||
};
|
||||
|
||||
settings: {
|
||||
read(): Promise<PluginSettings>;
|
||||
read<T = unknown>(key: string): Promise<T | undefined>;
|
||||
|
||||
+25
-1
@@ -1,7 +1,7 @@
|
||||
// Verstak Plugin SDK — Test Utilities
|
||||
|
||||
import type { PluginManifest, PluginState, RegisteredContributionPoints } from './types';
|
||||
import type { PluginCommandHandler, VerstakPluginAPI } from './plugin-api';
|
||||
import type { PluginCommandHandler, PluginLocale, TranslationParams, VerstakPluginAPI } from './plugin-api';
|
||||
|
||||
const mockCommandHandlers = new Map<string, PluginCommandHandler>();
|
||||
|
||||
@@ -11,6 +11,16 @@ function commandKey(pluginId: string, commandId: string): string {
|
||||
|
||||
export interface MockPluginAPIOptions {
|
||||
contributions?: RegisteredContributionPoints;
|
||||
locale?: PluginLocale;
|
||||
defaultLocale?: PluginLocale;
|
||||
messages?: Partial<Record<PluginLocale, Record<string, string>>>;
|
||||
}
|
||||
|
||||
function interpolateMessage(message: string, params?: TranslationParams): string {
|
||||
if (!params) return message;
|
||||
return message.replace(/\{([A-Za-z0-9_.-]+)\}/g, (placeholder, name: string) => (
|
||||
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : placeholder
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +61,9 @@ export function createTestPluginState(overrides?: Partial<PluginState>): PluginS
|
||||
* Создать заглушку VerstakPluginAPI для тестов.
|
||||
*/
|
||||
export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPluginAPIOptions = {}): VerstakPluginAPI {
|
||||
const locale = options.locale || 'en';
|
||||
const defaultLocale = options.defaultLocale || 'en';
|
||||
const messages = options.messages || {};
|
||||
const settings: Record<string, unknown> = {};
|
||||
const pluginData = new Map<string, Record<string, unknown>>();
|
||||
const commands = new Map<string, PluginCommandHandler>();
|
||||
@@ -139,6 +152,17 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
i18n: {
|
||||
getLocale: vi.fn(() => locale),
|
||||
t: vi.fn((key: string, params?: TranslationParams, fallback?: string) => {
|
||||
const message = messages[locale]?.[key]
|
||||
?? messages[defaultLocale]?.[key]
|
||||
?? fallback
|
||||
?? key;
|
||||
return interpolateMessage(message, params);
|
||||
}),
|
||||
onDidChangeLocale: vi.fn((_listener: (nextLocale: PluginLocale) => void) => () => {}),
|
||||
},
|
||||
settings: {
|
||||
read: vi.fn(async (key?: string) => key ? settings[key] : { ...settings }) as VerstakPluginAPI['settings']['read'],
|
||||
write: vi.fn(async (key: string, value: unknown) => {
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
export type PluginSource = 'official' | 'local' | 'third-party';
|
||||
|
||||
export interface PluginLocalizationConfig {
|
||||
defaultLocale: string;
|
||||
locales: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PluginManifest {
|
||||
schemaVersion: 1;
|
||||
id: string;
|
||||
@@ -13,6 +18,7 @@ export interface PluginManifest {
|
||||
description?: string;
|
||||
source?: PluginSource;
|
||||
icon?: string;
|
||||
localization?: PluginLocalizationConfig;
|
||||
provides: string[];
|
||||
requires?: string[];
|
||||
optionalRequires?: string[];
|
||||
|
||||
Reference in New Issue
Block a user