feat: migrate to _ locales handler

This commit is contained in:
divocat
2025-10-07 16:55:50 +03:00
parent e0874c3775
commit 9a72785fa7
25 changed files with 213 additions and 212 deletions

View File

@@ -9,7 +9,7 @@ export async function createBaseApiRequest<T>(
if (!response.ok) { if (!response.ok) {
return { return {
success: false as const, success: false as const,
message: `HTTP error ${response.status}: ${response.statusText}`, message: `${_('HTTP error')} ${response.status}: ${response.statusText}`,
}; };
} }
@@ -22,7 +22,7 @@ export async function createBaseApiRequest<T>(
} catch (e) { } catch (e) {
return { return {
success: false as const, success: false as const,
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : _('Unknown error'),
}; };
} }
} }

View File

@@ -1,29 +0,0 @@
interface CopyToClipboardResponse {
success: boolean;
message: string;
}
export function copyToClipboard(text: string): CopyToClipboardResponse {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
return {
success: true,
message: 'Copied!',
};
} catch (err) {
const error = err as Error;
return {
success: false,
message: `Failed to copy: ${error.message}`,
};
} finally {
document.body.removeChild(textarea);
}
}

View File

@@ -3,7 +3,6 @@ export * from './parseValueList';
export * from './injectGlobalStyles'; export * from './injectGlobalStyles';
export * from './withTimeout'; export * from './withTimeout';
export * from './executeShellCommand'; export * from './executeShellCommand';
export * from './copyToClipboard';
export * from './maskIP'; export * from './maskIP';
export * from './getProxyUrlName'; export * from './getProxyUrlName';
export * from './onMount'; export * from './onMount';

View File

@@ -2,7 +2,7 @@ export async function withTimeout<T>(
promise: Promise<T>, promise: Promise<T>,
timeoutMs: number, timeoutMs: number,
operationName: string, operationName: string,
timeoutMessage = 'Operation timed out', timeoutMessage = _('Operation timed out'),
): Promise<T> { ): Promise<T> {
let timeoutId; let timeoutId;
const start = performance.now(); const start = performance.now();

View File

@@ -33,6 +33,8 @@ declare global {
load: (packages: string | string[]) => Promise<string>; load: (packages: string | string[]) => Promise<string>;
sections: (conf: string, type?: string, cb?: () => void) => Promise<T>; sections: (conf: string, type?: string, cb?: () => void) => Promise<T>;
}; };
const _ = (_key: string) => string;
} }
export {}; export {};

View File

@@ -106,7 +106,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
outbounds: [ outbounds: [
{ {
code: outbound?.code || '', code: outbound?.code || '',
displayName: 'Fastest', displayName: _('Fastest'),
latency: outbound?.value?.history?.[0]?.delay || 0, latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '', type: outbound?.value?.type || '',
selected: selector?.value?.now === outbound?.code, selected: selector?.value?.now === outbound?.code,

View File

@@ -217,10 +217,10 @@ async function renderBandwidthWidget() {
const renderedWidget = renderWidget({ const renderedWidget = renderWidget({
loading: traffic.loading, loading: traffic.loading,
failed: traffic.failed, failed: traffic.failed,
title: 'Traffic', title: _('Traffic'),
items: [ items: [
{ key: 'Uplink', value: `${prettyBytes(traffic.data.up)}/s` }, { key: _('Uplink'), value: `${prettyBytes(traffic.data.up)}/s` },
{ key: 'Downlink', value: `${prettyBytes(traffic.data.down)}/s` }, { key: _('Downlink'), value: `${prettyBytes(traffic.data.down)}/s` },
], ],
}); });
@@ -247,14 +247,14 @@ async function renderTrafficTotalWidget() {
const renderedWidget = renderWidget({ const renderedWidget = renderWidget({
loading: trafficTotalWidget.loading, loading: trafficTotalWidget.loading,
failed: trafficTotalWidget.failed, failed: trafficTotalWidget.failed,
title: 'Traffic Total', title: _('Traffic Total'),
items: [ items: [
{ {
key: 'Uplink', key: _('Uplink'),
value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)), value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)),
}, },
{ {
key: 'Downlink', key: _('Downlink'),
value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)), value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)),
}, },
], ],
@@ -283,14 +283,14 @@ async function renderSystemInfoWidget() {
const renderedWidget = renderWidget({ const renderedWidget = renderWidget({
loading: systemInfoWidget.loading, loading: systemInfoWidget.loading,
failed: systemInfoWidget.failed, failed: systemInfoWidget.failed,
title: 'System info', title: _('System info'),
items: [ items: [
{ {
key: 'Active Connections', key: _('Active Connections'),
value: String(systemInfoWidget.data.connections), value: String(systemInfoWidget.data.connections),
}, },
{ {
key: 'Memory Usage', key: _('Memory Usage'),
value: String(prettyBytes(systemInfoWidget.data.memory)), value: String(prettyBytes(systemInfoWidget.data.memory)),
}, },
], ],
@@ -319,11 +319,13 @@ async function renderServicesInfoWidget() {
const renderedWidget = renderWidget({ const renderedWidget = renderWidget({
loading: servicesInfoWidget.loading, loading: servicesInfoWidget.loading,
failed: servicesInfoWidget.failed, failed: servicesInfoWidget.failed,
title: 'Services info', title: _('Services info'),
items: [ items: [
{ {
key: 'Podkop', key: _('Podkop'),
value: servicesInfoWidget.data.podkop ? '✔ Enabled' : '✘ Disabled', value: servicesInfoWidget.data.podkop
? _('✔ Enabled')
: _('✘ Disabled'),
attributes: { attributes: {
class: servicesInfoWidget.data.podkop class: servicesInfoWidget.data.podkop
? 'pdk_dashboard-page__widgets-section__item__row--success' ? 'pdk_dashboard-page__widgets-section__item__row--success'
@@ -331,8 +333,10 @@ async function renderServicesInfoWidget() {
}, },
}, },
{ {
key: 'Sing-box', key: _('Sing-box'),
value: servicesInfoWidget.data.singbox ? '✔ Running' : '✘ Stopped', value: servicesInfoWidget.data.singbox
? _('✔ Running')
: _('✘ Stopped'),
attributes: { attributes: {
class: servicesInfoWidget.data.singbox class: servicesInfoWidget.data.singbox
? 'pdk_dashboard-page__widgets-section__item__row--success' ? 'pdk_dashboard-page__widgets-section__item__row--success'

View File

@@ -15,7 +15,7 @@ function renderFailedState() {
class: 'pdk_dashboard-page__outbound-section centered', class: 'pdk_dashboard-page__outbound-section centered',
style: 'height: 127px', style: 'height: 127px',
}, },
E('span', {}, 'Dashboard currently unavailable'), E('span', {}, _('Dashboard currently unavailable')),
); );
} }

View File

@@ -19,7 +19,7 @@ function renderFailedState() {
style: 'height: 78px', style: 'height: 78px',
class: 'pdk_dashboard-page__widgets-section__item centered', class: 'pdk_dashboard-page__widgets-section__item centered',
}, },
'Currently unavailable', _('Currently unavailable'),
); );
} }

View File

@@ -4,20 +4,21 @@ import { ValidationResult } from './types';
export function validateDNS(value: string): ValidationResult { export function validateDNS(value: string): ValidationResult {
if (!value) { if (!value) {
return { valid: false, message: 'DNS server address cannot be empty' }; return { valid: false, message: _('DNS server address cannot be empty') };
} }
if (validateIPV4(value).valid) { if (validateIPV4(value).valid) {
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }
if (validateDomain(value).valid) { if (validateDomain(value).valid) {
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }
return { return {
valid: false, valid: false,
message: message: _(
'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH', 'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH',
),
}; };
} }

View File

@@ -5,7 +5,7 @@ export function validateDomain(domain: string): ValidationResult {
/^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/;
if (!domainRegex.test(domain)) { if (!domainRegex.test(domain)) {
return { valid: false, message: 'Invalid domain address' }; return { valid: false, message: _('Invalid domain address') };
} }
const hostname = domain.split('/')[0]; const hostname = domain.split('/')[0];
@@ -14,8 +14,8 @@ export function validateDomain(domain: string): ValidationResult {
const atLeastOneInvalidPart = parts.some((part) => part.length > 63); const atLeastOneInvalidPart = parts.some((part) => part.length > 63);
if (atLeastOneInvalidPart) { if (atLeastOneInvalidPart) {
return { valid: false, message: 'Invalid domain address' }; return { valid: false, message: _('Invalid domain address') };
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }

View File

@@ -5,8 +5,8 @@ export function validateIPV4(ip: string): ValidationResult {
/^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/; /^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
if (ipRegex.test(ip)) { if (ipRegex.test(ip)) {
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }
return { valid: false, message: 'Invalid IP address' }; return { valid: false, message: _('Invalid IP address') };
} }

View File

@@ -8,13 +8,14 @@ export function validateOutboundJson(value: string): ValidationResult {
if (!parsed.type || !parsed.server || !parsed.server_port) { if (!parsed.type || !parsed.server || !parsed.server_port) {
return { return {
valid: false, valid: false,
message: message: _(
'Outbound JSON must contain at least "type", "server" and "server_port" fields', 'Outbound JSON must contain at least "type", "server" and "server_port" fields',
),
}; };
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} catch { } catch {
return { valid: false, message: 'Invalid JSON format' }; return { valid: false, message: _('Invalid JSON format') };
} }
} }

View File

@@ -4,7 +4,7 @@ export function validatePath(value: string): ValidationResult {
if (!value) { if (!value) {
return { return {
valid: false, valid: false,
message: 'Path cannot be empty', message: _('Path cannot be empty'),
}; };
} }
@@ -19,7 +19,8 @@ export function validatePath(value: string): ValidationResult {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid path format. Path must start with "/" and contain valid characters', 'Invalid path format. Path must start with "/" and contain valid characters',
),
}; };
} }

View File

@@ -19,6 +19,6 @@ export function validateProxyUrl(url: string): ValidationResult {
return { return {
valid: false, valid: false,
message: 'URL must start with vless:// or ss:// or trojan://', message: _('URL must start with vless:// or ss:// or trojan://'),
}; };
} }

View File

@@ -5,7 +5,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (!url.startsWith('ss://')) { if (!url.startsWith('ss://')) {
return { return {
valid: false, valid: false,
message: 'Invalid Shadowsocks URL: must start with ss://', message: _('Invalid Shadowsocks URL: must start with ss://'),
}; };
} }
@@ -13,7 +13,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (!url || /\s/.test(url)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, valid: false,
message: 'Invalid Shadowsocks URL: must not contain spaces', message: _('Invalid Shadowsocks URL: must not contain spaces'),
}; };
} }
@@ -24,7 +24,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (!encryptedPart) { if (!encryptedPart) {
return { return {
valid: false, valid: false,
message: 'Invalid Shadowsocks URL: missing credentials', message: _('Invalid Shadowsocks URL: missing credentials'),
}; };
} }
@@ -34,16 +34,18 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (!decoded.includes(':')) { if (!decoded.includes(':')) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid Shadowsocks URL: decoded credentials must contain method:password', 'Invalid Shadowsocks URL: decoded credentials must contain method:password',
),
}; };
} }
} catch (_e) { } catch (_e) {
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) { if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid Shadowsocks URL: missing method and password separator ":"', 'Invalid Shadowsocks URL: missing method and password separator ":"',
),
}; };
} }
} }
@@ -53,7 +55,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (!serverPart) { if (!serverPart) {
return { return {
valid: false, valid: false,
message: 'Invalid Shadowsocks URL: missing server address', message: _('Invalid Shadowsocks URL: missing server address'),
}; };
} }
@@ -62,14 +64,17 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (!server) { if (!server) {
return { return {
valid: false, valid: false,
message: 'Invalid Shadowsocks URL: missing server', message: _('Invalid Shadowsocks URL: missing server'),
}; };
} }
const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null; const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
if (!port) { if (!port) {
return { valid: false, message: 'Invalid Shadowsocks URL: missing port' }; return {
valid: false,
message: _('Invalid Shadowsocks URL: missing port'),
};
} }
const portNum = parseInt(port, 10); const portNum = parseInt(port, 10);
@@ -77,12 +82,15 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (isNaN(portNum) || portNum < 1 || portNum > 65535) { if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return { return {
valid: false, valid: false,
message: 'Invalid port number. Must be between 1 and 65535', message: _('Invalid port number. Must be between 1 and 65535'),
}; };
} }
} catch (_e) { } catch (_e) {
return { valid: false, message: 'Invalid Shadowsocks URL: parsing failed' }; return {
valid: false,
message: _('Invalid Shadowsocks URL: parsing failed'),
};
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }

View File

@@ -8,14 +8,14 @@ export function validateSubnet(value: string): ValidationResult {
if (!subnetRegex.test(value)) { if (!subnetRegex.test(value)) {
return { return {
valid: false, valid: false,
message: 'Invalid format. Use X.X.X.X or X.X.X.X/Y', message: _('Invalid format. Use X.X.X.X or X.X.X.X/Y'),
}; };
} }
const [ip, cidr] = value.split('/'); const [ip, cidr] = value.split('/');
if (ip === '0.0.0.0') { if (ip === '0.0.0.0') {
return { valid: false, message: 'IP address 0.0.0.0 is not allowed' }; return { valid: false, message: _('IP address 0.0.0.0 is not allowed') };
} }
const ipCheck = validateIPV4(ip); const ipCheck = validateIPV4(ip);
@@ -30,10 +30,10 @@ export function validateSubnet(value: string): ValidationResult {
if (cidrNum < 0 || cidrNum > 32) { if (cidrNum < 0 || cidrNum > 32) {
return { return {
valid: false, valid: false,
message: 'CIDR must be between 0 and 32', message: _('CIDR must be between 0 and 32'),
}; };
} }
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }

View File

@@ -5,14 +5,14 @@ export function validateTrojanUrl(url: string): ValidationResult {
if (!url.startsWith('trojan://')) { if (!url.startsWith('trojan://')) {
return { return {
valid: false, valid: false,
message: 'Invalid Trojan URL: must start with trojan://', message: _('Invalid Trojan URL: must start with trojan://'),
}; };
} }
if (!url || /\s/.test(url)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, valid: false,
message: 'Invalid Trojan URL: must not contain spaces', message: _('Invalid Trojan URL: must not contain spaces'),
}; };
} }
@@ -22,12 +22,14 @@ export function validateTrojanUrl(url: string): ValidationResult {
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
return { return {
valid: false, valid: false,
message: 'Invalid Trojan URL: must contain username, hostname and port', message: _(
'Invalid Trojan URL: must contain username, hostname and port',
),
}; };
} }
} catch (_e) { } catch (_e) {
return { valid: false, message: 'Invalid Trojan URL: parsing failed' }; return { valid: false, message: _('Invalid Trojan URL: parsing failed') };
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }

View File

@@ -10,11 +10,11 @@ export function validateUrl(
if (!protocols.includes(parsedUrl.protocol)) { if (!protocols.includes(parsedUrl.protocol)) {
return { return {
valid: false, valid: false,
message: `URL must use one of the following protocols: ${protocols.join(', ')}`, message: `${_('URL must use one of the following protocols:')} ${protocols.join(', ')}`,
}; };
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} catch (_e) { } catch (_e) {
return { valid: false, message: 'Invalid URL format' }; return { valid: false, message: _('Invalid URL format') };
} }
} }

View File

@@ -7,27 +7,27 @@ export function validateVlessUrl(url: string): ValidationResult {
if (!url || /\s/.test(url)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, valid: false,
message: 'Invalid VLESS URL: must not contain spaces', message: _('Invalid VLESS URL: must not contain spaces'),
}; };
} }
if (parsedUrl.protocol !== 'vless:') { if (parsedUrl.protocol !== 'vless:') {
return { return {
valid: false, valid: false,
message: 'Invalid VLESS URL: must start with vless://', message: _('Invalid VLESS URL: must start with vless://'),
}; };
} }
if (!parsedUrl.username) { if (!parsedUrl.username) {
return { valid: false, message: 'Invalid VLESS URL: missing UUID' }; return { valid: false, message: _('Invalid VLESS URL: missing UUID') };
} }
if (!parsedUrl.hostname) { if (!parsedUrl.hostname) {
return { valid: false, message: 'Invalid VLESS URL: missing server' }; return { valid: false, message: _('Invalid VLESS URL: missing server') };
} }
if (!parsedUrl.port) { if (!parsedUrl.port) {
return { valid: false, message: 'Invalid VLESS URL: missing port' }; return { valid: false, message: _('Invalid VLESS URL: missing port') };
} }
if ( if (
@@ -37,15 +37,16 @@ export function validateVlessUrl(url: string): ValidationResult {
) { ) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: invalid port number. Must be between 1 and 65535', 'Invalid VLESS URL: invalid port number. Must be between 1 and 65535',
),
}; };
} }
if (!parsedUrl.search) { if (!parsedUrl.search) {
return { return {
valid: false, valid: false,
message: 'Invalid VLESS URL: missing query parameters', message: _('Invalid VLESS URL: missing query parameters'),
}; };
} }
@@ -67,8 +68,9 @@ export function validateVlessUrl(url: string): ValidationResult {
if (!type || !validTypes.includes(type)) { if (!type || !validTypes.includes(type)) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws', 'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws',
),
}; };
} }
@@ -78,8 +80,9 @@ export function validateVlessUrl(url: string): ValidationResult {
if (!security || !validSecurities.includes(security)) { if (!security || !validSecurities.includes(security)) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: security must be one of tls, reality, none', 'Invalid VLESS URL: security must be one of tls, reality, none',
),
}; };
} }
@@ -87,21 +90,23 @@ export function validateVlessUrl(url: string): ValidationResult {
if (!params.get('pbk')) { if (!params.get('pbk')) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: missing pbk parameter for reality security', 'Invalid VLESS URL: missing pbk parameter for reality security',
),
}; };
} }
if (!params.get('fp')) { if (!params.get('fp')) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: missing fp parameter for reality security', 'Invalid VLESS URL: missing fp parameter for reality security',
),
}; };
} }
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} catch (_e) { } catch (_e) {
return { valid: false, message: 'Invalid VLESS URL: parsing failed' }; return { valid: false, message: _('Invalid VLESS URL: parsing failed') };
} }
} }

View File

@@ -0,0 +1,2 @@
// tests/setup/global-mocks.ts
globalThis._ = (key: string) => key;

View File

@@ -4,5 +4,6 @@ export default defineConfig({
test: { test: {
globals: true, globals: true,
environment: 'node', environment: 'node',
setupFiles: ['./tests/setup/global-mocks.ts'],
}, },
}); });

View File

@@ -88,7 +88,7 @@ function createAdditionalSection(mainSection) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = mainSection.taboption( o = mainSection.taboption(
@@ -113,7 +113,7 @@ function createAdditionalSection(mainSection) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = mainSection.taboption( o = mainSection.taboption(
@@ -342,7 +342,7 @@ function createAdditionalSection(mainSection) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = mainSection.taboption( o = mainSection.taboption(

View File

@@ -152,7 +152,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
} catch (e) { } catch (e) {
return `${_('Invalid URL format:')} ${e?.message}`; return `${_('Invalid URL format:')} ${e?.message}`;
} }
@@ -180,7 +180,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -204,7 +204,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -315,7 +315,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -459,7 +459,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -538,7 +538,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -575,7 +575,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -612,7 +612,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -654,7 +654,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -733,7 +733,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
@@ -772,7 +772,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
} }

View File

@@ -8,40 +8,42 @@
function validateIPV4(ip) { function validateIPV4(ip) {
const ipRegex = /^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/; const ipRegex = /^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
if (ipRegex.test(ip)) { if (ipRegex.test(ip)) {
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} }
return { valid: false, message: "Invalid IP address" }; return { valid: false, message: _("Invalid IP address") };
} }
// src/validators/validateDomain.ts // src/validators/validateDomain.ts
function validateDomain(domain) { function validateDomain(domain) {
const domainRegex = /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; const domainRegex = /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/;
if (!domainRegex.test(domain)) { if (!domainRegex.test(domain)) {
return { valid: false, message: "Invalid domain address" }; return { valid: false, message: _("Invalid domain address") };
} }
const hostname = domain.split("/")[0]; const hostname = domain.split("/")[0];
const parts = hostname.split("."); const parts = hostname.split(".");
const atLeastOneInvalidPart = parts.some((part) => part.length > 63); const atLeastOneInvalidPart = parts.some((part) => part.length > 63);
if (atLeastOneInvalidPart) { if (atLeastOneInvalidPart) {
return { valid: false, message: "Invalid domain address" }; return { valid: false, message: _("Invalid domain address") };
} }
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} }
// src/validators/validateDns.ts // src/validators/validateDns.ts
function validateDNS(value) { function validateDNS(value) {
if (!value) { if (!value) {
return { valid: false, message: "DNS server address cannot be empty" }; return { valid: false, message: _("DNS server address cannot be empty") };
} }
if (validateIPV4(value).valid) { if (validateIPV4(value).valid) {
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} }
if (validateDomain(value).valid) { if (validateDomain(value).valid) {
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} }
return { return {
valid: false, valid: false,
message: "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" message: _(
"Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH"
)
}; };
} }
@@ -52,12 +54,12 @@ function validateUrl(url, protocols = ["http:", "https:"]) {
if (!protocols.includes(parsedUrl.protocol)) { if (!protocols.includes(parsedUrl.protocol)) {
return { return {
valid: false, valid: false,
message: `URL must use one of the following protocols: ${protocols.join(", ")}` message: `${_("URL must use one of the following protocols:")} ${protocols.join(", ")}`
}; };
} }
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} catch (_e) { } catch (_e) {
return { valid: false, message: "Invalid URL format" }; return { valid: false, message: _("Invalid URL format") };
} }
} }
@@ -66,7 +68,7 @@ function validatePath(value) {
if (!value) { if (!value) {
return { return {
valid: false, valid: false,
message: "Path cannot be empty" message: _("Path cannot be empty")
}; };
} }
const pathRegex = /^\/[a-zA-Z0-9_\-/.]+$/; const pathRegex = /^\/[a-zA-Z0-9_\-/.]+$/;
@@ -78,7 +80,9 @@ function validatePath(value) {
} }
return { return {
valid: false, valid: false,
message: 'Invalid path format. Path must start with "/" and contain valid characters' message: _(
'Invalid path format. Path must start with "/" and contain valid characters'
)
}; };
} }
@@ -88,12 +92,12 @@ function validateSubnet(value) {
if (!subnetRegex.test(value)) { if (!subnetRegex.test(value)) {
return { return {
valid: false, valid: false,
message: "Invalid format. Use X.X.X.X or X.X.X.X/Y" message: _("Invalid format. Use X.X.X.X or X.X.X.X/Y")
}; };
} }
const [ip, cidr] = value.split("/"); const [ip, cidr] = value.split("/");
if (ip === "0.0.0.0") { if (ip === "0.0.0.0") {
return { valid: false, message: "IP address 0.0.0.0 is not allowed" }; return { valid: false, message: _("IP address 0.0.0.0 is not allowed") };
} }
const ipCheck = validateIPV4(ip); const ipCheck = validateIPV4(ip);
if (!ipCheck.valid) { if (!ipCheck.valid) {
@@ -104,11 +108,11 @@ function validateSubnet(value) {
if (cidrNum < 0 || cidrNum > 32) { if (cidrNum < 0 || cidrNum > 32) {
return { return {
valid: false, valid: false,
message: "CIDR must be between 0 and 32" message: _("CIDR must be between 0 and 32")
}; };
} }
} }
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} }
// src/validators/bulkValidate.ts // src/validators/bulkValidate.ts
@@ -125,14 +129,14 @@ function validateShadowsocksUrl(url) {
if (!url.startsWith("ss://")) { if (!url.startsWith("ss://")) {
return { return {
valid: false, valid: false,
message: "Invalid Shadowsocks URL: must start with ss://" message: _("Invalid Shadowsocks URL: must start with ss://")
}; };
} }
try { try {
if (!url || /\s/.test(url)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, valid: false,
message: "Invalid Shadowsocks URL: must not contain spaces" message: _("Invalid Shadowsocks URL: must not contain spaces")
}; };
} }
const mainPart = url.includes("?") ? url.split("?")[0] : url.split("#")[0]; const mainPart = url.includes("?") ? url.split("?")[0] : url.split("#")[0];
@@ -140,7 +144,7 @@ function validateShadowsocksUrl(url) {
if (!encryptedPart) { if (!encryptedPart) {
return { return {
valid: false, valid: false,
message: "Invalid Shadowsocks URL: missing credentials" message: _("Invalid Shadowsocks URL: missing credentials")
}; };
} }
try { try {
@@ -148,14 +152,18 @@ function validateShadowsocksUrl(url) {
if (!decoded.includes(":")) { if (!decoded.includes(":")) {
return { return {
valid: false, valid: false,
message: "Invalid Shadowsocks URL: decoded credentials must contain method:password" message: _(
"Invalid Shadowsocks URL: decoded credentials must contain method:password"
)
}; };
} }
} catch (_e) { } catch (_e) {
if (!encryptedPart.includes(":") && !encryptedPart.includes("-")) { if (!encryptedPart.includes(":") && !encryptedPart.includes("-")) {
return { return {
valid: false, valid: false,
message: 'Invalid Shadowsocks URL: missing method and password separator ":"' message: _(
'Invalid Shadowsocks URL: missing method and password separator ":"'
)
}; };
} }
} }
@@ -163,31 +171,37 @@ function validateShadowsocksUrl(url) {
if (!serverPart) { if (!serverPart) {
return { return {
valid: false, valid: false,
message: "Invalid Shadowsocks URL: missing server address" message: _("Invalid Shadowsocks URL: missing server address")
}; };
} }
const [server, portAndRest] = serverPart.split(":"); const [server, portAndRest] = serverPart.split(":");
if (!server) { if (!server) {
return { return {
valid: false, valid: false,
message: "Invalid Shadowsocks URL: missing server" message: _("Invalid Shadowsocks URL: missing server")
}; };
} }
const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null; const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
if (!port) { if (!port) {
return { valid: false, message: "Invalid Shadowsocks URL: missing port" }; return {
valid: false,
message: _("Invalid Shadowsocks URL: missing port")
};
} }
const portNum = parseInt(port, 10); const portNum = parseInt(port, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) { if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return { return {
valid: false, valid: false,
message: "Invalid port number. Must be between 1 and 65535" message: _("Invalid port number. Must be between 1 and 65535")
}; };
} }
} catch (_e) { } catch (_e) {
return { valid: false, message: "Invalid Shadowsocks URL: parsing failed" }; return {
valid: false,
message: _("Invalid Shadowsocks URL: parsing failed")
};
} }
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} }
// src/validators/validateVlessUrl.ts // src/validators/validateVlessUrl.ts
@@ -197,34 +211,36 @@ function validateVlessUrl(url) {
if (!url || /\s/.test(url)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, valid: false,
message: "Invalid VLESS URL: must not contain spaces" message: _("Invalid VLESS URL: must not contain spaces")
}; };
} }
if (parsedUrl.protocol !== "vless:") { if (parsedUrl.protocol !== "vless:") {
return { return {
valid: false, valid: false,
message: "Invalid VLESS URL: must start with vless://" message: _("Invalid VLESS URL: must start with vless://")
}; };
} }
if (!parsedUrl.username) { if (!parsedUrl.username) {
return { valid: false, message: "Invalid VLESS URL: missing UUID" }; return { valid: false, message: _("Invalid VLESS URL: missing UUID") };
} }
if (!parsedUrl.hostname) { if (!parsedUrl.hostname) {
return { valid: false, message: "Invalid VLESS URL: missing server" }; return { valid: false, message: _("Invalid VLESS URL: missing server") };
} }
if (!parsedUrl.port) { if (!parsedUrl.port) {
return { valid: false, message: "Invalid VLESS URL: missing port" }; return { valid: false, message: _("Invalid VLESS URL: missing port") };
} }
if (isNaN(+parsedUrl.port) || +parsedUrl.port < 1 || +parsedUrl.port > 65535) { if (isNaN(+parsedUrl.port) || +parsedUrl.port < 1 || +parsedUrl.port > 65535) {
return { return {
valid: false, valid: false,
message: "Invalid VLESS URL: invalid port number. Must be between 1 and 65535" message: _(
"Invalid VLESS URL: invalid port number. Must be between 1 and 65535"
)
}; };
} }
if (!parsedUrl.search) { if (!parsedUrl.search) {
return { return {
valid: false, valid: false,
message: "Invalid VLESS URL: missing query parameters" message: _("Invalid VLESS URL: missing query parameters")
}; };
} }
const params = new URLSearchParams(parsedUrl.search); const params = new URLSearchParams(parsedUrl.search);
@@ -243,7 +259,9 @@ function validateVlessUrl(url) {
if (!type || !validTypes.includes(type)) { if (!type || !validTypes.includes(type)) {
return { return {
valid: false, valid: false,
message: "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws" message: _(
"Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws"
)
}; };
} }
const security = params.get("security"); const security = params.get("security");
@@ -251,26 +269,32 @@ function validateVlessUrl(url) {
if (!security || !validSecurities.includes(security)) { if (!security || !validSecurities.includes(security)) {
return { return {
valid: false, valid: false,
message: "Invalid VLESS URL: security must be one of tls, reality, none" message: _(
"Invalid VLESS URL: security must be one of tls, reality, none"
)
}; };
} }
if (security === "reality") { if (security === "reality") {
if (!params.get("pbk")) { if (!params.get("pbk")) {
return { return {
valid: false, valid: false,
message: "Invalid VLESS URL: missing pbk parameter for reality security" message: _(
"Invalid VLESS URL: missing pbk parameter for reality security"
)
}; };
} }
if (!params.get("fp")) { if (!params.get("fp")) {
return { return {
valid: false, valid: false,
message: "Invalid VLESS URL: missing fp parameter for reality security" message: _(
"Invalid VLESS URL: missing fp parameter for reality security"
)
}; };
} }
} }
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} catch (_e) { } catch (_e) {
return { valid: false, message: "Invalid VLESS URL: parsing failed" }; return { valid: false, message: _("Invalid VLESS URL: parsing failed") };
} }
} }
@@ -281,12 +305,14 @@ function validateOutboundJson(value) {
if (!parsed.type || !parsed.server || !parsed.server_port) { if (!parsed.type || !parsed.server || !parsed.server_port) {
return { return {
valid: false, valid: false,
message: 'Outbound JSON must contain at least "type", "server" and "server_port" fields' message: _(
'Outbound JSON must contain at least "type", "server" and "server_port" fields'
)
}; };
} }
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} catch { } catch {
return { valid: false, message: "Invalid JSON format" }; return { valid: false, message: _("Invalid JSON format") };
} }
} }
@@ -295,13 +321,13 @@ function validateTrojanUrl(url) {
if (!url.startsWith("trojan://")) { if (!url.startsWith("trojan://")) {
return { return {
valid: false, valid: false,
message: "Invalid Trojan URL: must start with trojan://" message: _("Invalid Trojan URL: must start with trojan://")
}; };
} }
if (!url || /\s/.test(url)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, valid: false,
message: "Invalid Trojan URL: must not contain spaces" message: _("Invalid Trojan URL: must not contain spaces")
}; };
} }
try { try {
@@ -309,13 +335,15 @@ function validateTrojanUrl(url) {
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
return { return {
valid: false, valid: false,
message: "Invalid Trojan URL: must contain username, hostname and port" message: _(
"Invalid Trojan URL: must contain username, hostname and port"
)
}; };
} }
} catch (_e) { } catch (_e) {
return { valid: false, message: "Invalid Trojan URL: parsing failed" }; return { valid: false, message: _("Invalid Trojan URL: parsing failed") };
} }
return { valid: true, message: "Valid" }; return { valid: true, message: _("Valid") };
} }
// src/validators/validateProxyUrl.ts // src/validators/validateProxyUrl.ts
@@ -331,7 +359,7 @@ function validateProxyUrl(url) {
} }
return { return {
valid: false, valid: false,
message: "URL must start with vless:// or ss:// or trojan://" message: _("URL must start with vless:// or ss:// or trojan://")
}; };
} }
@@ -537,10 +565,10 @@ function injectGlobalStyles() {
} }
// src/helpers/withTimeout.ts // src/helpers/withTimeout.ts
async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = "Operation timed out") { async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = _("Operation timed out")) {
let timeoutId; let timeoutId;
const start = performance.now(); const start = performance.now();
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_2, reject) => {
timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
}); });
try { try {
@@ -680,29 +708,6 @@ async function executeShellCommand({
} }
} }
// src/helpers/copyToClipboard.ts
function copyToClipboard(text) {
const textarea = document.createElement("textarea");
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
return {
success: true,
message: "Copied!"
};
} catch (err) {
const error = err;
return {
success: false,
message: `Failed to copy: ${error.message}`
};
} finally {
document.body.removeChild(textarea);
}
}
// src/helpers/maskIP.ts // src/helpers/maskIP.ts
function maskIP(ip = "") { function maskIP(ip = "") {
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
@@ -767,7 +772,7 @@ async function createBaseApiRequest(fetchFn) {
if (!response.ok) { if (!response.ok) {
return { return {
success: false, success: false,
message: `HTTP error ${response.status}: ${response.statusText}` message: `${_("HTTP error")} ${response.status}: ${response.statusText}`
}; };
} }
const data = await response.json(); const data = await response.json();
@@ -778,7 +783,7 @@ async function createBaseApiRequest(fetchFn) {
} catch (e) { } catch (e) {
return { return {
success: false, success: false,
message: e instanceof Error ? e.message : "Unknown error" message: e instanceof Error ? e.message : _("Unknown error")
}; };
} }
} }
@@ -943,7 +948,7 @@ async function getDashboardSections() {
outbounds: [ outbounds: [
{ {
code: outbound?.code || "", code: outbound?.code || "",
displayName: "Fastest", displayName: _("Fastest"),
latency: outbound?.value?.history?.[0]?.delay || 0, latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || "", type: outbound?.value?.type || "",
selected: selector?.value?.now === outbound?.code selected: selector?.value?.now === outbound?.code
@@ -1080,7 +1085,7 @@ var TabServiceInstance = TabService.getInstance();
// src/store.ts // src/store.ts
function jsonStableStringify(obj) { function jsonStableStringify(obj) {
return JSON.stringify(obj, (_, value) => { return JSON.stringify(obj, (_2, value) => {
if (value && typeof value === "object" && !Array.isArray(value)) { if (value && typeof value === "object" && !Array.isArray(value)) {
return Object.keys(value).sort().reduce( return Object.keys(value).sort().reduce(
(acc, key) => { (acc, key) => {
@@ -1214,7 +1219,7 @@ function renderFailedState() {
class: "pdk_dashboard-page__outbound-section centered", class: "pdk_dashboard-page__outbound-section centered",
style: "height: 127px" style: "height: 127px"
}, },
E("span", {}, "Dashboard currently unavailable") E("span", {}, _("Dashboard currently unavailable"))
); );
} }
function renderLoadingState() { function renderLoadingState() {
@@ -1318,7 +1323,7 @@ function renderFailedState2() {
style: "height: 78px", style: "height: 78px",
class: "pdk_dashboard-page__widgets-section__item centered" class: "pdk_dashboard-page__widgets-section__item centered"
}, },
"Currently unavailable" _("Currently unavailable")
); );
} }
function renderLoadingState2() { function renderLoadingState2() {
@@ -1713,10 +1718,10 @@ async function renderBandwidthWidget() {
const renderedWidget = renderWidget({ const renderedWidget = renderWidget({
loading: traffic.loading, loading: traffic.loading,
failed: traffic.failed, failed: traffic.failed,
title: "Traffic", title: _("Traffic"),
items: [ items: [
{ key: "Uplink", value: `${prettyBytes(traffic.data.up)}/s` }, { key: _("Uplink"), value: `${prettyBytes(traffic.data.up)}/s` },
{ key: "Downlink", value: `${prettyBytes(traffic.data.down)}/s` } { key: _("Downlink"), value: `${prettyBytes(traffic.data.down)}/s` }
] ]
}); });
container.replaceChildren(renderedWidget); container.replaceChildren(renderedWidget);
@@ -1737,14 +1742,14 @@ async function renderTrafficTotalWidget() {
const renderedWidget = renderWidget({ const renderedWidget = renderWidget({
loading: trafficTotalWidget.loading, loading: trafficTotalWidget.loading,
failed: trafficTotalWidget.failed, failed: trafficTotalWidget.failed,
title: "Traffic Total", title: _("Traffic Total"),
items: [ items: [
{ {
key: "Uplink", key: _("Uplink"),
value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)) value: String(prettyBytes(trafficTotalWidget.data.uploadTotal))
}, },
{ {
key: "Downlink", key: _("Downlink"),
value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)) value: String(prettyBytes(trafficTotalWidget.data.downloadTotal))
} }
] ]
@@ -1767,14 +1772,14 @@ async function renderSystemInfoWidget() {
const renderedWidget = renderWidget({ const renderedWidget = renderWidget({
loading: systemInfoWidget.loading, loading: systemInfoWidget.loading,
failed: systemInfoWidget.failed, failed: systemInfoWidget.failed,
title: "System info", title: _("System info"),
items: [ items: [
{ {
key: "Active Connections", key: _("Active Connections"),
value: String(systemInfoWidget.data.connections) value: String(systemInfoWidget.data.connections)
}, },
{ {
key: "Memory Usage", key: _("Memory Usage"),
value: String(prettyBytes(systemInfoWidget.data.memory)) value: String(prettyBytes(systemInfoWidget.data.memory))
} }
] ]
@@ -1797,18 +1802,18 @@ async function renderServicesInfoWidget() {
const renderedWidget = renderWidget({ const renderedWidget = renderWidget({
loading: servicesInfoWidget.loading, loading: servicesInfoWidget.loading,
failed: servicesInfoWidget.failed, failed: servicesInfoWidget.failed,
title: "Services info", title: _("Services info"),
items: [ items: [
{ {
key: "Podkop", key: _("Podkop"),
value: servicesInfoWidget.data.podkop ? "\u2714 Enabled" : "\u2718 Disabled", value: servicesInfoWidget.data.podkop ? _("\u2714 Enabled") : _("\u2718 Disabled"),
attributes: { attributes: {
class: servicesInfoWidget.data.podkop ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" class: servicesInfoWidget.data.podkop ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error"
} }
}, },
{ {
key: "Sing-box", key: _("Sing-box"),
value: servicesInfoWidget.data.singbox ? "\u2714 Running" : "\u2718 Stopped", value: servicesInfoWidget.data.singbox ? _("\u2714 Running") : _("\u2718 Stopped"),
attributes: { attributes: {
class: servicesInfoWidget.data.singbox ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" class: servicesInfoWidget.data.singbox ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error"
} }
@@ -1865,7 +1870,6 @@ return baseclass.extend({
TabServiceInstance, TabServiceInstance,
UPDATE_INTERVAL_OPTIONS, UPDATE_INTERVAL_OPTIONS,
bulkValidate, bulkValidate,
copyToClipboard,
coreService, coreService,
createBaseApiRequest, createBaseApiRequest,
executeShellCommand, executeShellCommand,