mirror of
https://github.com/HeyPuter/puter.git
synced 2025-02-02 23:28:39 +08:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
3bd32bb13b
@ -21,7 +21,6 @@ const { Kernel } = require("./src/Kernel.js");
|
||||
const DatabaseModule = require("./src/DatabaseModule.js");
|
||||
const LocalDiskStorageModule = require("./src/LocalDiskStorageModule.js");
|
||||
const SelfHostedModule = require("./src/modules/selfhosted/SelfHostedModule.js");
|
||||
const PuterDriversModule = require("./src/PuterDriversModule.js");
|
||||
const { testlaunch } = require("./src/index.js");
|
||||
const BaseService = require("./src/services/BaseService.js");
|
||||
const { Context } = require("./src/util/context.js");
|
||||
@ -32,6 +31,7 @@ const { WebModule } = require("./src/modules/web/WebModule.js");
|
||||
const { Core2Module } = require("./src/modules/core/Core2Module.js");
|
||||
const { TemplateModule } = require("./src/modules/template/TemplateModule.js");
|
||||
const { PuterFSModule } = require("./src/modules/puterfs/PuterFSModule.js");
|
||||
const { PerfMonModule } = require("./src/modules/perfmon/PerfMonModule.js");
|
||||
|
||||
|
||||
module.exports = {
|
||||
@ -59,10 +59,12 @@ module.exports = {
|
||||
CoreModule,
|
||||
WebModule,
|
||||
DatabaseModule,
|
||||
PuterDriversModule,
|
||||
LocalDiskStorageModule,
|
||||
SelfHostedModule,
|
||||
TestDriversModule,
|
||||
PuterAIModule,
|
||||
BroadcastModule,
|
||||
|
||||
// Development modules
|
||||
PerfMonModule,
|
||||
};
|
||||
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
|
||||
class PuterDriversModule extends AdvancedBase {
|
||||
async install () {}
|
||||
async install_legacy (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const { DriverService } = require("./services/drivers/DriverService");
|
||||
services.registerService('driver', DriverService);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PuterDriversModule;
|
@ -324,36 +324,6 @@ module.exports = class APIError {
|
||||
message: () => 'Invalid token.',
|
||||
},
|
||||
|
||||
// drivers
|
||||
'interface_not_found': {
|
||||
status: 404,
|
||||
message: ({ interface_name }) => `Interface not found: ${quot(interface_name)}`,
|
||||
},
|
||||
'no_implementation_available': {
|
||||
status: 502,
|
||||
message: ({
|
||||
iface,
|
||||
interface_name,
|
||||
driver
|
||||
}) => `No implementation available for ` +
|
||||
(iface ?? interface_name) ? 'interface' : 'driver' +
|
||||
' ' + quot(iface ?? interface_name ?? driver) + '.',
|
||||
},
|
||||
'method_not_found': {
|
||||
status: 404,
|
||||
message: ({ interface_name, method_name }) => `Method not found: ${quot(method_name)} on interface ${quot(interface_name)}`,
|
||||
},
|
||||
'missing_required_argument': {
|
||||
status: 400,
|
||||
message: ({ interface_name, method_name, arg_name }) =>
|
||||
`Missing required argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}`,
|
||||
},
|
||||
'argument_consolidation_failed': {
|
||||
status: 400,
|
||||
message: ({ interface_name, method_name, arg_name, message }) =>
|
||||
`Failed to parse or process argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}: ${message}`,
|
||||
},
|
||||
|
||||
// SLA
|
||||
'rate_limit_exceeded': {
|
||||
status: 429,
|
||||
@ -505,18 +475,6 @@ module.exports = class APIError {
|
||||
status: 400,
|
||||
message: 'Incorrect or missing anti-CSRF token.',
|
||||
},
|
||||
|
||||
// Chat
|
||||
// TODO: specifying these errors here might be a violation
|
||||
// of separation of concerns. Services could register their
|
||||
// own errors with an error registry.
|
||||
'max_tokens_exceeded': {
|
||||
status: 400,
|
||||
message: ({ input_tokens, max_tokens }) =>
|
||||
`Input exceeds maximum token count. ` +
|
||||
`Input has ${input_tokens} tokens, ` +
|
||||
`but the maximum is ${max_tokens}.`,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -17,9 +17,6 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
module.exports = class UserParam {
|
||||
constructor () {
|
||||
//
|
||||
}
|
||||
consolidate ({ req }) {
|
||||
return req.user;
|
||||
}
|
||||
|
@ -140,15 +140,15 @@ if ( config.os.refined ) {
|
||||
module.exports = config;
|
||||
|
||||
// NEW_CONFIG_LOADING
|
||||
const maybe_port = config =>
|
||||
config.pub_port !== 80 && config.pub_port !== 443 ? ':' + config.pub_port : '';
|
||||
|
||||
const computed_defaults = {
|
||||
pub_port: config => config.http_port,
|
||||
origin: config => config.protocol + '://' + config.domain +
|
||||
(config.pub_port !== 80 && config.pub_port !== 443 ? ':' + config.pub_port : ''),
|
||||
origin: config => config.protocol + '://' + config.domain + maybe_port(config),
|
||||
api_base_url: config => config.experimental_no_subdomain
|
||||
? config.origin
|
||||
: config.protocol + '://api.' + config.domain +
|
||||
(config.pub_port !== 80 && config.pub_port !== 443 ? ':' + config.pub_port : ''),
|
||||
: config.protocol + '://api.' + config.domain + maybe_port(config),
|
||||
social_card: config => `${config.origin}/assets/img/screenshot.png`,
|
||||
};
|
||||
|
||||
|
@ -84,17 +84,17 @@ const hardcoded_user_group_permissions = {
|
||||
'service:hello-world:ii:hello-world': policy_perm('temp.es'),
|
||||
'service:puter-kvstore:ii:puter-kvstore': policy_perm('temp.kv'),
|
||||
'driver:puter-kvstore': policy_perm('temp.kv'),
|
||||
'driver:puter-notifications': policy_perm('temp.es'),
|
||||
'driver:puter-apps': policy_perm('temp.es'),
|
||||
'driver:puter-subdomains': policy_perm('temp.es'),
|
||||
'service:puter-notifications:ii:crud-q': policy_perm('temp.es'),
|
||||
'service:puter-apps:ii:crud-q': policy_perm('temp.es'),
|
||||
'service:puter-subdomains:ii:crud-q': policy_perm('temp.es'),
|
||||
},
|
||||
'78b1b1dd-c959-44d2-b02c-8735671f9997': {
|
||||
'service:hello-world:ii:hello-world': policy_perm('user.es'),
|
||||
'service:puter-kvstore:ii:puter-kvstore': policy_perm('user.kv'),
|
||||
'driver:puter-kvstore': policy_perm('user.kv'),
|
||||
'driver:puter-notifications': policy_perm('user.es'),
|
||||
'driver:puter-apps': policy_perm('user.es'),
|
||||
'driver:puter-subdomains': policy_perm('user.es'),
|
||||
'service:es\\Cnotification:ii:crud-q': policy_perm('user.es'),
|
||||
'service:es\\Capp:ii:crud-q': policy_perm('user.es'),
|
||||
'service:es\\Csubdomain:ii:crud-q': policy_perm('user.es'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,190 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
const { Context } = require('../util/context')
|
||||
const APIError = require("../api/APIError");
|
||||
const { AppUnderUserActorType, UserActorType } = require("../services/auth/Actor");
|
||||
const { BaseOperation } = require("../services/OperationTraceService");
|
||||
const { CodeUtil } = require("../codex/CodeUtil");
|
||||
|
||||
/**
|
||||
* Base class for all driver implementations.
|
||||
*
|
||||
* @deprecated - we use traits on services now. This class is kept for compatibility
|
||||
* with EntityStoreImplementation and DBKVStore which still use this.
|
||||
*/
|
||||
class Driver extends AdvancedBase {
|
||||
constructor (...a) {
|
||||
super(...a);
|
||||
const methods = this._get_merged_static_object('METHODS');
|
||||
// Turn each method into an operation
|
||||
for ( const k in methods ) {
|
||||
methods[k] = CodeUtil.mrwrap(methods[k], BaseOperation, {
|
||||
name: `${this.constructor.ID}:${k}`,
|
||||
});
|
||||
};
|
||||
this.methods = methods;
|
||||
this.sla = this._get_merged_static_object('SLA');
|
||||
}
|
||||
|
||||
async call (method, args) {
|
||||
if ( ! this.methods[method] ) {
|
||||
throw new Error(`method not found: ${method}`);
|
||||
}
|
||||
|
||||
const pseudo_this = Object.assign({}, this);
|
||||
|
||||
const context = Context.get();
|
||||
pseudo_this.context = context;
|
||||
pseudo_this.services = context.get('services');
|
||||
const services = context.get('services');
|
||||
pseudo_this.log = services.get('log-service').create(this.constructor.name);
|
||||
|
||||
await this._sla_enforcement(method);
|
||||
|
||||
return await this.methods[method].call(pseudo_this, args);
|
||||
}
|
||||
|
||||
async _sla_enforcement (method) {
|
||||
const context = Context.get();
|
||||
const services = context.get('services');
|
||||
const method_key = `${this.constructor.ID}:${method}`;
|
||||
const svc_sla = services.get('sla');
|
||||
|
||||
// System SLA enforcement
|
||||
{
|
||||
const sla_key = `driver:impl:${method_key}`;
|
||||
const sla = await svc_sla.get('system', sla_key);
|
||||
|
||||
const sys_method_key = `system:${method_key}`;
|
||||
|
||||
// short-term rate limiting
|
||||
if ( sla?.rate_limit ) {
|
||||
const svc_rateLimit = services.get('rate-limit');
|
||||
let eventual_success = false;
|
||||
for ( let i = 0 ; i < 60 ; i++ ) {
|
||||
try {
|
||||
await svc_rateLimit.check_and_increment(sys_method_key, sla.rate_limit.max, sla.rate_limit.period);
|
||||
eventual_success = true;
|
||||
break;
|
||||
} catch ( e ) {
|
||||
if (
|
||||
! ( e instanceof APIError ) ||
|
||||
e.fields.code !== 'rate_limit_exceeded'
|
||||
) throw e;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
if ( ! eventual_success ) {
|
||||
throw APIError.create('server_rate_exceeded');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test_mode is checked to prevent rate limiting when it is enabled
|
||||
const test_mode = context.get('test_mode');
|
||||
|
||||
// User SLA enforcement
|
||||
{
|
||||
const actor = context.get('actor').get_related_actor(UserActorType);
|
||||
|
||||
const user_is_verified = !! actor.type.user.email_confirmed;
|
||||
|
||||
const sla_key = `driver:impl:${method_key}`;
|
||||
const sla = await svc_sla.get(
|
||||
user_is_verified ? 'user_verified' : 'user_unverified',
|
||||
sla_key
|
||||
);
|
||||
|
||||
// short-term rate limiting
|
||||
if ( sla?.rate_limit ) {
|
||||
const svc_rateLimit = services.get('rate-limit');
|
||||
await svc_rateLimit.check_and_increment(method_key, sla.rate_limit.max, sla.rate_limit.period);
|
||||
}
|
||||
|
||||
// long-term rate limiting
|
||||
if ( sla?.monthly_limit && ! test_mode ) {
|
||||
const svc_monthlyUsage = services.get('monthly-usage');
|
||||
const count = await svc_monthlyUsage.check(
|
||||
actor, {
|
||||
'driver.interface': this.constructor.INTERFACE,
|
||||
'driver.implementation': this.constructor.ID,
|
||||
'driver.method': method,
|
||||
});
|
||||
if ( count >= sla.monthly_limit ) {
|
||||
throw APIError.create('monthly_limit_exceeded', null, {
|
||||
method_key,
|
||||
limit: sla.monthly_limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// App SLA enforcement
|
||||
await (async () => {
|
||||
const actor = context.get('actor');
|
||||
if ( ! ( actor.type instanceof AppUnderUserActorType ) ) return;
|
||||
|
||||
const sla_key = `driver:impl:${method_key}`;
|
||||
const sla = await svc_sla.get('app_default', sla_key);
|
||||
|
||||
// long-term rate limiting
|
||||
if ( sla?.monthly_limit && ! test_mode ) {
|
||||
const svc_monthlyUsage = services.get('monthly-usage');
|
||||
const count = await svc_monthlyUsage.check(
|
||||
actor, {
|
||||
'driver.interface': this.constructor.INTERFACE,
|
||||
'driver.implementation': this.constructor.ID,
|
||||
'driver.method': method,
|
||||
});
|
||||
if ( count >= sla.monthly_limit ) {
|
||||
throw APIError.create('monthly_limit_exceeded', null, {
|
||||
method_key,
|
||||
limit: sla.monthly_limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Record monthly usage
|
||||
if ( ! test_mode ) {
|
||||
const actor = context.get('actor');
|
||||
const svc_monthlyUsage = services.get('monthly-usage');
|
||||
const extra = {
|
||||
'driver.interface': this.constructor.INTERFACE,
|
||||
'driver.implementation': this.constructor.ID,
|
||||
'driver.method': method,
|
||||
...(this.get_usage_extra ? this.get_usage_extra() : {}),
|
||||
};
|
||||
await svc_monthlyUsage.increment(actor, method_key, extra);
|
||||
}
|
||||
}
|
||||
|
||||
async get_response_meta () {
|
||||
return {
|
||||
driver: this.constructor.ID,
|
||||
driver_version: this.constructor.VERSION,
|
||||
driver_interface: this.constructor.INTERFACE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Driver,
|
||||
};
|
@ -1,217 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const config = require("../config");
|
||||
const APIError = require("../api/APIError");
|
||||
const { DB_READ, DB_WRITE } = require("../services/database/consts");
|
||||
const { Driver } = require("../definitions/Driver");
|
||||
const { get_app } = require("../helpers");
|
||||
|
||||
class DBKVStore extends Driver {
|
||||
static ID = 'public-db-kvstore';
|
||||
static VERSION = '0.0.0';
|
||||
static INTERFACE = 'puter-kvstore';
|
||||
static MODULES = {
|
||||
murmurhash: require('murmurhash'),
|
||||
}
|
||||
static METHODS = {
|
||||
get: async function ({ app_uid, key }) {
|
||||
const actor = this.context.get('actor');
|
||||
|
||||
// If the actor is an app then it gets its own KV store.
|
||||
// The way this is implemented isn't ideal for future behaviour;
|
||||
// a KV implementation specified by the user would have parameters
|
||||
// that are scoped to the app, so this should eventually be
|
||||
// changed to get the app ID from the same interface that would
|
||||
// be used to obtain per-app user-specified implementation params.
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
const db = this.services.get('database').get(DB_READ, 'kvstore');
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
const kv = app ? await db.read(
|
||||
`SELECT * FROM kv WHERE user_id=? AND app=? AND kkey_hash=? LIMIT 1`,
|
||||
[ user.id, app.uid, key_hash ]
|
||||
) : await db.read(
|
||||
`SELECT * FROM kv WHERE user_id=? AND (app IS NULL OR app = 'global') AND kkey_hash=? LIMIT 1`,
|
||||
[ user.id, key_hash ]
|
||||
);
|
||||
|
||||
if ( kv[0] ) kv[0].value = db.case({
|
||||
mysql: () => kv[0].value,
|
||||
otherwise: () => JSON.parse(kv[0].value ?? 'null'),
|
||||
})();
|
||||
|
||||
return kv[0]?.value ?? null;
|
||||
},
|
||||
set: async function ({ app_uid, key, value }) {
|
||||
const actor = this.context.get('actor');
|
||||
|
||||
// Validate the key
|
||||
// get() doesn't String() the key but it only passes it to
|
||||
// murmurhash.v3() so it doesn't need to ¯\_(ツ)_/¯
|
||||
key = String(key);
|
||||
if ( Buffer.byteLength(key, 'utf8') > config.kv_max_key_size ) {
|
||||
throw new Error(`key is too large. Max size is ${config.kv_max_key_size}.`);
|
||||
}
|
||||
|
||||
// Validate the value
|
||||
value = value === undefined ? null : value;
|
||||
if (
|
||||
value !== null &&
|
||||
Buffer.byteLength(JSON.stringify(value), 'utf8') >
|
||||
config.kv_max_value_size
|
||||
) {
|
||||
throw new Error(`value is too large. Max size is ${config.kv_max_value_size}.`);
|
||||
}
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
const db = this.services.get('database').get(DB_WRITE, 'kvstore');
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
|
||||
try {
|
||||
await db.write(
|
||||
`INSERT INTO kv (user_id, app, kkey_hash, kkey, value)
|
||||
VALUES (?, ?, ?, ?, ?) ` +
|
||||
db.case({
|
||||
mysql: 'ON DUPLICATE KEY UPDATE value = ?',
|
||||
sqlite: 'ON CONFLICT(user_id, app, kkey_hash) DO UPDATE SET value = excluded.value',
|
||||
}),
|
||||
[
|
||||
user.id, app?.uid ?? 'global', key_hash, key,
|
||||
JSON.stringify(value),
|
||||
...db.case({ mysql: [value], otherwise: [] }),
|
||||
]
|
||||
);
|
||||
} catch (e) {
|
||||
// I discovered that my .sqlite file was corrupted and the update
|
||||
// above didn't work. The current database initialization does not
|
||||
// cause this issue so I'm adding this log as a safeguard.
|
||||
// - KernelDeimos / ED
|
||||
const svc_error = this.services.get('error-service');
|
||||
svc_error.report('kvstore:sqlite_error', {
|
||||
message: 'Broken database version - please contact maintainers',
|
||||
source: e,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
del: async function ({ app_uid, key }) {
|
||||
const actor = this.context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
const db = this.services.get('database').get(DB_WRITE, 'kvstore');
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
|
||||
await db.write(
|
||||
`DELETE FROM kv WHERE user_id=? AND app=? AND kkey_hash=?`,
|
||||
[ user.id, app?.uid ?? 'global', key_hash ]
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
list: async function ({ app_uid, as }) {
|
||||
const actor = this.context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
const db = this.services.get('database').get(DB_READ, 'kvstore');
|
||||
let rows = app ? await db.read(
|
||||
`SELECT kkey, value FROM kv WHERE user_id=? AND app=?`,
|
||||
[ user.id, app.uid ]
|
||||
) : await db.read(
|
||||
`SELECT kkey, value FROM kv WHERE user_id=? AND (app IS NULL OR app = 'global')`,
|
||||
[ user.id ]
|
||||
);
|
||||
|
||||
rows = rows.map(row => ({
|
||||
key: row.kkey,
|
||||
value: db.case({
|
||||
mysql: () => row.value,
|
||||
otherwise: () => JSON.parse(row.value ?? 'null')
|
||||
})(),
|
||||
}));
|
||||
|
||||
as = as || 'entries';
|
||||
|
||||
if ( ! ['keys','values','entries'].includes(as) ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'as',
|
||||
expected: '"keys", "values", or "entries"',
|
||||
});
|
||||
}
|
||||
|
||||
if ( as === 'keys' ) rows = rows.map(row => row.key);
|
||||
else if ( as === 'values' ) rows = rows.map(row => row.value);
|
||||
|
||||
return rows;
|
||||
},
|
||||
flush: async function ({ app_uid }) {
|
||||
const actor = this.context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
const db = this.services.get('database').get(DB_WRITE, 'kvstore');
|
||||
|
||||
await db.write(
|
||||
`DELETE FROM kv WHERE user_id=? AND app=?`,
|
||||
[ user.id, app?.uid ?? 'global' ]
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DBKVStore,
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const APIError = require("../api/APIError");
|
||||
const { Driver } = require("../definitions/Driver");
|
||||
const { Entity } = require("../om/entitystorage/Entity");
|
||||
const { Or, And, Eq } = require("../om/query/query");
|
||||
|
||||
const _fetch_based_on_complex_id = async (self, id) => {
|
||||
// Ensure `id` is an object and get its keys
|
||||
if ( ! id || typeof id !== 'object' || Array.isArray(id) ) {
|
||||
throw APIError.create('invalid_id', null, { id });
|
||||
}
|
||||
|
||||
const id_keys = Object.keys(id);
|
||||
// sort keys alphabetically
|
||||
id_keys.sort();
|
||||
|
||||
// Ensure key set is valid based on redundant keys listing
|
||||
const svc_es = self.services.get(self.service);
|
||||
const redundant_identifiers = svc_es.om.redundant_identifiers ?? [];
|
||||
|
||||
let match_found = false;
|
||||
for ( let key of redundant_identifiers ) {
|
||||
// Either a single key or a list
|
||||
key = Array.isArray(key) ? key : [key];
|
||||
|
||||
// All keys in the list must be present in the id
|
||||
for ( let i=0 ; i < key.length ; i++ ) {
|
||||
if ( ! id_keys.includes(key[i]) ) {
|
||||
break;
|
||||
}
|
||||
if ( i === key.length - 1 ) {
|
||||
match_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! match_found ) {
|
||||
throw APIError.create('invalid_id', null, { id });
|
||||
}
|
||||
|
||||
// Construct a query predicate based on the keys
|
||||
const key_eqs = [];
|
||||
for ( const key of id_keys ) {
|
||||
key_eqs.push(new Eq({
|
||||
key,
|
||||
value: id[key],
|
||||
}));
|
||||
}
|
||||
let predicate = new And({ children: key_eqs });
|
||||
|
||||
// Perform a select
|
||||
const entity = await svc_es.read({ predicate });
|
||||
if ( ! entity ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure there is only one result
|
||||
return entity;
|
||||
}
|
||||
|
||||
const _fetch_based_on_either_id = async (self, uid, id) => {
|
||||
if ( uid ) {
|
||||
const svc_es = self.services.get(self.service);
|
||||
return await svc_es.read(uid);
|
||||
}
|
||||
|
||||
return await _fetch_based_on_complex_id(self, id);
|
||||
}
|
||||
|
||||
class EntityStoreImplementation extends Driver {
|
||||
constructor ({ service }) {
|
||||
super();
|
||||
this.service = service;
|
||||
}
|
||||
get_usage_extra () {
|
||||
return {
|
||||
['driver.interface']: 'puter-es',
|
||||
['driver.implementation']: 'puter-es:' + this.service,
|
||||
};
|
||||
}
|
||||
static METHODS = {
|
||||
create: async function ({ object, options }) {
|
||||
const svc_es = this.services.get(this.service);
|
||||
if ( object.hasOwnProperty(svc_es.om.primary_identifier) ) {
|
||||
throw APIError.create('field_not_allowed_for_create', null, { key: svc_es.om.primary_identifier });
|
||||
}
|
||||
const entity = await Entity.create({ om: svc_es.om }, object);
|
||||
return await svc_es.create(entity, options);
|
||||
},
|
||||
update: async function ({ object, id, options }) {
|
||||
const svc_es = this.services.get(this.service);
|
||||
// if ( ! object.hasOwnProperty(svc_es.om.primary_identifier) ) {
|
||||
// throw APIError.create('field_required_for_update', null, { key: svc_es.om.primary_identifier });
|
||||
// }
|
||||
const entity = await Entity.create({ om: svc_es.om }, object);
|
||||
return await svc_es.update(entity, id, options);
|
||||
},
|
||||
upsert: async function ({ object, id, options }) {
|
||||
const svc_es = this.services.get(this.service);
|
||||
const entity = await Entity.create({ om: svc_es.om }, object);
|
||||
return await svc_es.upsert(entity, id, options);
|
||||
},
|
||||
read: async function ({ uid, id }) {
|
||||
if ( ! uid && ! id ) {
|
||||
throw APIError.create('xor_field_missing', null, {
|
||||
names: ['uid', 'id'],
|
||||
});
|
||||
}
|
||||
|
||||
const entity = await _fetch_based_on_either_id(this, uid, id);
|
||||
if ( ! entity ) {
|
||||
throw APIError.create('entity_not_found', null, {
|
||||
identifier: uid
|
||||
});
|
||||
}
|
||||
return await entity.get_client_safe();
|
||||
},
|
||||
select: async function (options) {
|
||||
const svc_es = this.services.get(this.service);
|
||||
const entities = await svc_es.select(options);
|
||||
const client_safe_entities = [];
|
||||
for ( const entity of entities ) {
|
||||
client_safe_entities.push(await entity.get_client_safe());
|
||||
}
|
||||
return client_safe_entities;
|
||||
},
|
||||
delete: async function ({ uid, id }) {
|
||||
if ( ! uid && ! id ) {
|
||||
throw APIError.create('xor_field_missing', null, {
|
||||
names: ['uid', 'id'],
|
||||
});
|
||||
}
|
||||
|
||||
if ( id && ! uid ) {
|
||||
const entity = await _fetch_based_on_complex_id(this, id);
|
||||
if ( ! entity ) {
|
||||
throw APIError.create('entity_not_found', null, {
|
||||
identifier: id
|
||||
});
|
||||
}
|
||||
const svc_es = this.services.get(this.service);
|
||||
uid = await entity.get(svc_es.om.primary_identifier);
|
||||
}
|
||||
|
||||
const svc_es = this.services.get(this.service);
|
||||
return await svc_es.delete(uid);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EntityStoreImplementation,
|
||||
};
|
@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { Driver } = require("../definitions/Driver");
|
||||
|
||||
class HelloWorld extends Driver {
|
||||
static ID = 'public-helloworld';
|
||||
static VERSION = '0.0.0';
|
||||
static INTERFACE = 'helloworld';
|
||||
static SLA = {
|
||||
greet: {
|
||||
rate_limit: {
|
||||
max: 10,
|
||||
period: 30000,
|
||||
},
|
||||
monthly_limit: Math.pow(1, 6),
|
||||
},
|
||||
}
|
||||
static METHODS = {
|
||||
greet: async function ({ subject }) {
|
||||
return `Hello, ${subject ?? 'World'}!`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HelloWorld,
|
||||
};
|
@ -50,7 +50,7 @@ const error_help_details = [
|
||||
apply (more) {
|
||||
more.references = [
|
||||
...reused.runtime_env_references,
|
||||
]
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -68,10 +68,10 @@ const error_help_details = [
|
||||
{
|
||||
title: 'Set CONFIG_PATH or RUNTIME_PATH environment variable',
|
||||
},
|
||||
],
|
||||
];
|
||||
more.references = [
|
||||
...reused.runtime_env_references,
|
||||
]
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -83,7 +83,7 @@ const error_help_details = [
|
||||
{
|
||||
title: 'Create a valid config file',
|
||||
},
|
||||
]
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -112,7 +112,7 @@ const error_help_details = [
|
||||
use: 'describes why this error occurs',
|
||||
url: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_const_assignment'
|
||||
},
|
||||
]
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -122,7 +122,7 @@ const error_help_details = [
|
||||
apply (more) {
|
||||
more.notes = [
|
||||
'It looks like this might be our fault.',
|
||||
]
|
||||
];
|
||||
more.solutions = [
|
||||
{
|
||||
title: `Check for an issue on ` +
|
||||
@ -135,7 +135,7 @@ const error_help_details = [
|
||||
'create one'
|
||||
) + '.'
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -18,10 +18,8 @@
|
||||
*/
|
||||
// TODO: database access can be a service
|
||||
const { RESOURCE_STATUS_PENDING_CREATE } = require('../modules/puterfs/ResourceService.js');
|
||||
const DatabaseFSEntryFetcher = require("./storage/DatabaseFSEntryFetcher");
|
||||
const { TraceService } = require('../services/TraceService.js');
|
||||
const FSAccessContext = require('./FSAccessContext.js');
|
||||
const SystemFSEntryService = require('./storage/SystemFSEntryService.js');
|
||||
const PerformanceMonitor = require('../monitor/PerformanceMonitor.js');
|
||||
const { NodePathSelector, NodeUIDSelector, NodeInternalIDSelector } = require('./node/selectors.js');
|
||||
const FSNodeContext = require('./FSNodeContext.js');
|
||||
@ -50,14 +48,7 @@ class FilesystemService extends BaseService {
|
||||
|
||||
services.registerService('traceService', TraceService);
|
||||
|
||||
// TODO: [fs:remove-separate-updater-and-fetcher]
|
||||
services.set('fsEntryFetcher', new DatabaseFSEntryFetcher({
|
||||
services: services,
|
||||
}));
|
||||
|
||||
// The new fs entry service
|
||||
services.registerService('systemFSEntryService', SystemFSEntryService);
|
||||
|
||||
this.log = services.get('log-service').create('filesystem-service');
|
||||
|
||||
// used by update_child_paths
|
||||
@ -72,27 +63,6 @@ class FilesystemService extends BaseService {
|
||||
.obtain('fs.fsentry:path')
|
||||
.exec(entry.uuid);
|
||||
});
|
||||
|
||||
|
||||
// Decorate methods with otel span
|
||||
// TODO: use putility class feature for method decorators
|
||||
const span_methods = [
|
||||
'write', 'mkdir', 'rm', 'mv', 'cp', 'read', 'stat',
|
||||
'mkdir_2',
|
||||
'update_child_paths',
|
||||
];
|
||||
for ( const method of span_methods ) {
|
||||
const original_method = this[method];
|
||||
this[method] = async (...args) => {
|
||||
const tracer = services.get('traceService').tracer;
|
||||
let result;
|
||||
await tracer.startActiveSpan(`fs-svc:${method}`, async span => {
|
||||
result = await original_method.call(this, ...args);
|
||||
span.end();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _init () {
|
||||
@ -252,7 +222,7 @@ class FilesystemService extends BaseService {
|
||||
await target.fetchEntry({ thumbnail: true });
|
||||
|
||||
const { _path, uuidv4 } = this.modules;
|
||||
const systemFSEntryService = this.services.get('systemFSEntryService');
|
||||
const svc_fsEntry = this.services.get('fsEntryService');
|
||||
|
||||
const ts = Math.round(Date.now() / 1000);
|
||||
const uid = uuidv4();
|
||||
@ -282,7 +252,7 @@ class FilesystemService extends BaseService {
|
||||
|
||||
this.log.debug('creating fsentry', { fsentry: raw_fsentry })
|
||||
|
||||
const entryOp = await systemFSEntryService.insert(raw_fsentry);
|
||||
const entryOp = await svc_fsEntry.insert(raw_fsentry);
|
||||
|
||||
console.log('entry op', entryOp);
|
||||
|
||||
@ -319,7 +289,7 @@ class FilesystemService extends BaseService {
|
||||
|
||||
const { _path, uuidv4 } = this.modules;
|
||||
const resourceService = this.services.get('resourceService');
|
||||
const systemFSEntryService = this.services.get('systemFSEntryService');
|
||||
const svc_fsEntry = this.services.get('fsEntryService');
|
||||
|
||||
const ts = Math.round(Date.now() / 1000);
|
||||
const uid = uuidv4();
|
||||
@ -346,7 +316,7 @@ class FilesystemService extends BaseService {
|
||||
|
||||
this.log.debug('creating symlink', { fsentry: raw_fsentry })
|
||||
|
||||
const entryOp = await systemFSEntryService.insert(raw_fsentry);
|
||||
const entryOp = await svc_fsEntry.insert(raw_fsentry);
|
||||
|
||||
(async () => {
|
||||
await entryOp.awaitDone();
|
||||
|
@ -157,15 +157,15 @@ class LLCopy extends LLFilesystemOperation {
|
||||
status: RESOURCE_STATUS_PENDING_CREATE,
|
||||
});
|
||||
|
||||
const svc_fsentry = svc.get('systemFSEntryService');
|
||||
const svc_fsEntry = svc.get('fsEntryService');
|
||||
this.log.info(`inserting entry: ` + uuid);
|
||||
const entryOp = await svc_fsentry.insert(raw_fsentry);
|
||||
const entryOp = await svc_fsEntry.insert(raw_fsentry);
|
||||
|
||||
let node;
|
||||
|
||||
this.checkpoint('before parallel tasks');
|
||||
const tasks = new ParallelTasks({ tracer, max: 4 });
|
||||
await tracer.startActiveSpan(`fs:cp:parallel-portion`, async span => {
|
||||
await Context.arun(`fs:cp:parallel-portion`, async () => {
|
||||
this.checkpoint('starting parallel tasks');
|
||||
// Add child copy tasks if this is a directory
|
||||
if ( source.entry.is_dir ) {
|
||||
@ -216,8 +216,6 @@ class LLCopy extends LLFilesystemOperation {
|
||||
this.checkpoint('waiting for parallel tasks');
|
||||
await tasks.awaitAll();
|
||||
this.checkpoint('finishing up');
|
||||
|
||||
span.end();
|
||||
});
|
||||
|
||||
node = node || await fs.node(new NodeUIDSelector(uuid));
|
||||
|
@ -60,7 +60,7 @@ class LLMkdir extends LLFilesystemOperation {
|
||||
const ts = Math.round(Date.now() / 1000);
|
||||
const uid = uuidv4();
|
||||
const resourceService = context.get('services').get('resourceService');
|
||||
const systemFSEntryService = context.get('services').get('systemFSEntryService');
|
||||
const svc_fsEntry = context.get('services').get('fsEntryService');
|
||||
const svc_event = context.get('services').get('event');
|
||||
const fs = context.get('services').get('filesystem');
|
||||
|
||||
@ -109,7 +109,7 @@ class LLMkdir extends LLFilesystemOperation {
|
||||
this.log.debug('creating fsentry', { fsentry: raw_fsentry })
|
||||
|
||||
this.checkpoint('about to enqueue insert');
|
||||
const entryOp = await systemFSEntryService.insert(raw_fsentry);
|
||||
const entryOp = await svc_fsEntry.insert(raw_fsentry);
|
||||
|
||||
this.field('fsentry-created', false);
|
||||
|
||||
|
@ -136,7 +136,7 @@ class LLOWrite extends LLWriteBase {
|
||||
const svc = Context.get('services');
|
||||
const sizeService = svc.get('sizeService');
|
||||
const resourceService = svc.get('resourceService');
|
||||
const systemFSEntryService = svc.get('systemFSEntryService');
|
||||
const svc_fsEntry = svc.get('fsEntryService');
|
||||
const svc_event = svc.get('event');
|
||||
|
||||
// TODO: fs:decouple-versions
|
||||
@ -188,7 +188,7 @@ class LLOWrite extends LLWriteBase {
|
||||
const filesize = file.size;
|
||||
sizeService.change_usage(actor.type.user.id, filesize);
|
||||
|
||||
const entryOp = await systemFSEntryService.update(uid, raw_fsentry_delta);
|
||||
const entryOp = await svc_fsEntry.update(uid, raw_fsentry_delta);
|
||||
|
||||
// depends on fsentry, does not depend on S3
|
||||
(async () => {
|
||||
@ -235,7 +235,7 @@ class LLCWrite extends LLWriteBase {
|
||||
const svc = Context.get('services');
|
||||
const sizeService = svc.get('sizeService');
|
||||
const resourceService = svc.get('resourceService');
|
||||
const systemFSEntryService = svc.get('systemFSEntryService');
|
||||
const svc_fsEntry = svc.get('fsEntryService');
|
||||
const svc_event = svc.get('event');
|
||||
const fs = svc.get('filesystem');
|
||||
|
||||
@ -317,7 +317,7 @@ class LLCWrite extends LLWriteBase {
|
||||
|
||||
this.checkpoint('after change_usage');
|
||||
|
||||
const entryOp = await systemFSEntryService.insert(raw_fsentry);
|
||||
const entryOp = await svc_fsEntry.insert(raw_fsentry);
|
||||
|
||||
this.checkpoint('after fsentry insert enqueue');
|
||||
|
||||
|
@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { PuterPath } = require("../lib/PuterPath");
|
||||
const _path = require('path');
|
||||
|
||||
// Redis keys:
|
||||
// <env>:<service>:<class>:<type>:<property>:<id>
|
||||
//
|
||||
// note: <environment> is added by redisService automatically.
|
||||
//
|
||||
// If `<type>` is `multi`, then the format differs slightly:
|
||||
// <env>:<service>:<class>:multi:<type>:<property>:<id-property>:<id>
|
||||
// where `<id-property>` specifies the property being used for the id
|
||||
|
||||
class SystemFSEntryService {
|
||||
constructor ({ services }) {
|
||||
this.redis = { enabled: false };
|
||||
this.DatabaseFSEntryService = services.get('fsEntryService');
|
||||
|
||||
this.log = services.get('log-service').create('system-fsentry-service');
|
||||
|
||||
// Register information providers
|
||||
const info = services.get('information');
|
||||
this.info = info;
|
||||
|
||||
if ( ! this.redis.enabled ) return;
|
||||
|
||||
// path -> uuid via redis
|
||||
info.given('fs.fsentry:path').provide('fs.fsentry:uuid')
|
||||
.addStrategy('redis', async path => {
|
||||
return await this.get_uuid_from_path(path);
|
||||
});
|
||||
// uuid -> path via redis
|
||||
info.given('fs.fsentry:uuid').provide('fs.fsentry:path')
|
||||
.addStrategy('redis', async uuid => {
|
||||
this.log.debug('getting path for: ' + uuid);
|
||||
if ( uuid === PuterPath.NULL_UUID ) return '/';
|
||||
const res = ( await this.redis.get(`fs:fsentry:path:path:${uuid}`) ) ?? undefined;
|
||||
this.log.debug('got path: ' + res);
|
||||
return res;
|
||||
});
|
||||
// uuid -> parent_uuid via redis
|
||||
info.given('fs.fsentry:uuid').provide('fs.fsentry:children(fs.fsentry:uuid)')
|
||||
.addStrategy('redis', async uuid => {
|
||||
return await this.get_child_uuids(uuid);
|
||||
});
|
||||
}
|
||||
|
||||
async insert (entry) {
|
||||
if ( this.redis.enabled ) {
|
||||
await this._link(entry.uuid, entry.parent_uid, entry.name);
|
||||
}
|
||||
return await this.DatabaseFSEntryService.insert(entry);
|
||||
}
|
||||
|
||||
async update (uuid, entry) {
|
||||
// If parent_uid is set during an update, we assume that it
|
||||
// has been changed. If it hasn't, no problem: just an extra
|
||||
// cache invalidation; but the code that set it should know
|
||||
// better because it probably has the fsentry data already.
|
||||
if ( entry.hasOwnProperty('parent_uid') ) {
|
||||
await this._relocate(uuid, entry.parent_uid)
|
||||
}
|
||||
return await this.DatabaseFSEntryService.update(uuid, entry);
|
||||
}
|
||||
|
||||
async delete (uuid) {
|
||||
//
|
||||
}
|
||||
|
||||
async get_child_uuids (uuid) {
|
||||
let members;
|
||||
members = await this.redis.smembers(`fs:fsentry:set:childs:${uuid}`);
|
||||
if ( members ) return members;
|
||||
members = await this.DatabaseFSEntryService.get_descendants(uuid);
|
||||
return members ?? [];
|
||||
}
|
||||
|
||||
async get_uuid_from_path (path) {
|
||||
path = PuterPath.adapt(path);
|
||||
|
||||
let pathOfReference = path.reference === PuterPath.NULL_UUID
|
||||
? '/' : this.get_path_from_uuid(path.reference);
|
||||
|
||||
const fullPath = _path.join(pathOfReference, path.relativePortion);
|
||||
let uuid = await this.redis.get(`fs:fsentry:multi:uuid:uuid:path:${fullPath}`);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// Cache related functions
|
||||
async _link (subject_uuid, parent_uuid, subject_name) {
|
||||
this.log.info(`linking ${subject_uuid} to ${parent_uuid}`);
|
||||
// We need the parent's path to update everything
|
||||
|
||||
let pathOfParent = await this.info.with('fs.fsentry:uuid')
|
||||
.obtain('fs.fsentry:path').exec(parent_uuid);
|
||||
|
||||
this.log.debug(`path of parent: ${pathOfParent}`);
|
||||
|
||||
if ( ! subject_name ) {
|
||||
subject_name = await this.redis.get(`fs:fsentry:str:name:${subject_uuid}`);
|
||||
}
|
||||
|
||||
// Register properties
|
||||
await this.redis.set(`fs:fsentry:uuid:parent:${subject_uuid}`, parent_uuid);
|
||||
await this.redis.set(`fs:fsentry:str:name:${subject_uuid}`, subject_name);
|
||||
|
||||
// Add as child of parent
|
||||
await this.redis.sadd(`fs:fsentry:set:childs:${parent_uuid}`, subject_uuid);
|
||||
|
||||
// Register path
|
||||
const subject_path = `${pathOfParent}/${subject_name}`;
|
||||
this.log.debug(`registering path: ${subject_path} for ${subject_uuid}`);
|
||||
await this.redis.set(`fs:fsentry:path:path:${subject_uuid}`, subject_path);
|
||||
await this.redis.set(`fs:fsentry:multi:uuid:uuid:path:${subject_path}`, subject_uuid);
|
||||
}
|
||||
|
||||
async _unlink (subject_uuid) {
|
||||
let parent_uuid = await this.redis.get(`fs:fsentry:uuid:parent:${subject_uuid}`);
|
||||
// TODO: try getting from database
|
||||
|
||||
// Remove from parent
|
||||
await this.redis.srem(`fs:fsentry:set:childs:${parent_uuid}`, subject_uuid);
|
||||
}
|
||||
|
||||
async _purge (subject_uuid) {
|
||||
await this._unlink(subject_uuid);
|
||||
|
||||
// Remove properties
|
||||
await this.redis.del(`fs:fsentry:uuid:parent:${subject_uuid}`);
|
||||
await this.redis.del(`fs:fsentry:str:name:${subject_uuid}`);
|
||||
|
||||
// Remove path
|
||||
const subject_path =
|
||||
await this.redis.get(`fs:fsentry:path:path:${subject_uuid}`);
|
||||
await this.redis.del(`fs:fsentry:path:path:${subject_uuid}`);
|
||||
if ( subject_path ) {
|
||||
await this.redis.del(`fs:fsentry:multi:uuid:path:${subject_path}`);
|
||||
}
|
||||
}
|
||||
|
||||
async _relocate (subject_uuid, new_parent_uuid) {
|
||||
await this._unlink(subject_uuid);
|
||||
await this._link(subject_uuid, new_parent_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SystemFSEntryService;
|
@ -1115,6 +1115,7 @@ async function jwt_auth(req){
|
||||
}
|
||||
|
||||
return {
|
||||
actor,
|
||||
user: actor.type.user,
|
||||
token: token,
|
||||
};
|
||||
|
21
src/backend/src/modules/core/ContextService.js
Normal file
21
src/backend/src/modules/core/ContextService.js
Normal file
@ -0,0 +1,21 @@
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { Context } = require("../../util/context");
|
||||
|
||||
/**
|
||||
* ContextService provides a way for other services to register a hook to be
|
||||
* called when a context/subcontext is created.
|
||||
*
|
||||
* Contexts are used to provide contextual information in the execution
|
||||
* context (dynamic scope). They can also be used to identify a "span";
|
||||
* a span is a labelled frame of execution that can be used to track
|
||||
* performance, errors, and other metrics.
|
||||
*/
|
||||
class ContextService extends BaseService {
|
||||
register_context_hook (event, hook) {
|
||||
Context.context_hooks_[event].push(hook);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ContextService,
|
||||
};
|
@ -50,6 +50,9 @@ class Core2Module extends AdvancedBase {
|
||||
|
||||
const { ParameterService } = require("./ParameterService.js");
|
||||
services.registerService('params', ParameterService);
|
||||
|
||||
const { ContextService } = require('./ContextService.js');
|
||||
services.registerService('context', ContextService);
|
||||
}
|
||||
}
|
||||
|
||||
|
20
src/backend/src/modules/perfmon/PerfMonModule.js
Normal file
20
src/backend/src/modules/perfmon/PerfMonModule.js
Normal file
@ -0,0 +1,20 @@
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
|
||||
/**
|
||||
* Enable this module when you want performance monitoring.
|
||||
*
|
||||
* Performance monitoring requires additional setup. Jaegar should be installed
|
||||
* and running.
|
||||
*/
|
||||
class PerfMonModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const TelemetryService = require("./TelemetryService");
|
||||
services.registerService('telemetry', TelemetryService);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PerfMonModule,
|
||||
};
|
@ -16,6 +16,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const opentelemetry = require("@opentelemetry/api");
|
||||
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
||||
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
||||
const { PeriodicExportingMetricReader, ConsoleMetricExporter } = require('@opentelemetry/sdk-metrics');
|
||||
@ -25,17 +26,13 @@ const { SemanticResourceAttributes } = require("@opentelemetry/semantic-conventi
|
||||
const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
|
||||
const { ConsoleSpanExporter, BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base");
|
||||
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
|
||||
const config = require('../config');
|
||||
const config = require('../../config');
|
||||
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
|
||||
|
||||
class TelemetryService {
|
||||
static instance_ = null;
|
||||
static getInstance () {
|
||||
if ( this.instance_ ) return this.instance_;
|
||||
return this.instance_ = new TelemetryService();
|
||||
}
|
||||
const BaseService = require('../../services/BaseService');
|
||||
|
||||
constructor () {
|
||||
class TelemetryService extends BaseService {
|
||||
_construct () {
|
||||
const resource = Resource.default().merge(
|
||||
new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: "puter-backend",
|
||||
@ -61,6 +58,33 @@ class TelemetryService {
|
||||
});
|
||||
|
||||
this.sdk = sdk;
|
||||
|
||||
this.sdk.start();
|
||||
|
||||
this.tracer_ = opentelemetry.trace.getTracer(
|
||||
'puter-tracer'
|
||||
);
|
||||
}
|
||||
|
||||
_init () {
|
||||
const svc_context = this.services.get('context');
|
||||
svc_context.register_context_hook('pre_arun', ({ hints, trace_name, callback, replace_callback }) => {
|
||||
if ( ! trace_name ) return;
|
||||
if ( ! hints.trace ) return;
|
||||
console.log('APPLYING TRACE NAME', trace_name);
|
||||
replace_callback(async () => {
|
||||
return await this.tracer_.startActiveSpan(trace_name, async span => {
|
||||
try {
|
||||
return await callback();
|
||||
} catch (error) {
|
||||
span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR, message: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getConfiguredExporter_() {
|
||||
@ -69,12 +93,6 @@ class TelemetryService {
|
||||
}
|
||||
const exporter = new ConsoleSpanExporter();
|
||||
}
|
||||
|
||||
start () {
|
||||
// this.sdk.start();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TelemetryService
|
||||
}
|
||||
module.exports = TelemetryService;
|
@ -6,6 +6,7 @@ const { DB_WRITE } = require("../../services/database/consts");
|
||||
const { TypeSpec } = require("../../services/drivers/meta/Construct");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const { Context } = require("../../util/context");
|
||||
const { AsModeration } = require("./lib/AsModeration");
|
||||
|
||||
// Maximum number of fallback attempts when a model fails, including the first attempt
|
||||
const MAX_FALLBACKS = 3 + 1; // includes first attempt
|
||||
@ -92,6 +93,17 @@ class AIChatService extends BaseService {
|
||||
|
||||
await this.db.insert('ai_usage', values);
|
||||
});
|
||||
|
||||
const svc_apiErrpr = this.services.get('api-error');
|
||||
svc_apiErrpr.register({
|
||||
max_tokens_exceeded: {
|
||||
status: 400,
|
||||
message: ({ input_tokens, max_tokens }) =>
|
||||
`Input exceeds maximum token count. ` +
|
||||
`Input has ${input_tokens} tokens, ` +
|
||||
`but the maximum is ${max_tokens}.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -478,11 +490,6 @@ class AIChatService extends BaseService {
|
||||
* Returns true if OpenAI service is unavailable or all messages pass moderation.
|
||||
*/
|
||||
async moderate ({ messages }) {
|
||||
const svc_openai = this.services.get('openai-completion');
|
||||
|
||||
// We can't use moderation of openai service isn't available
|
||||
if ( ! svc_openai ) return true;
|
||||
|
||||
for ( const msg of messages ) {
|
||||
const texts = [];
|
||||
if ( typeof msg.content === 'string' ) texts.push(msg.content);
|
||||
@ -497,8 +504,41 @@ class AIChatService extends BaseService {
|
||||
|
||||
const fulltext = texts.join('\n');
|
||||
|
||||
const mod_result = await svc_openai.check_moderation(fulltext);
|
||||
if ( mod_result.flagged ) return false;
|
||||
let mod_last_error = null;
|
||||
let mod_result = null;
|
||||
try {
|
||||
const svc_openai = this.services.get('openai-completion');
|
||||
mod_result = await svc_openai.check_moderation(fulltext);
|
||||
if ( mod_result.flagged ) return false;
|
||||
continue;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
mod_last_error = e;
|
||||
}
|
||||
try {
|
||||
const svc_claude = this.services.get('claude');
|
||||
const chat = svc_claude.as('puter-chat-completion');
|
||||
const mod = new AsModeration({
|
||||
chat,
|
||||
model: 'claude-3-haiku-20240307',
|
||||
})
|
||||
if ( ! await mod.moderate(fulltext) ) {
|
||||
return false;
|
||||
}
|
||||
mod_last_error = null;
|
||||
continue;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
mod_last_error = e;
|
||||
}
|
||||
|
||||
if ( mod_last_error ) {
|
||||
this.log.error('moderation error', {
|
||||
fulltext,
|
||||
mod_last_error,
|
||||
});
|
||||
throw new Error('no working moderation service');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -606,10 +646,6 @@ class AIChatService extends BaseService {
|
||||
let model = parameters.model;
|
||||
if ( ! model ) {
|
||||
const service = this.services.get(intended_service);
|
||||
console.log({
|
||||
what: intended_service,
|
||||
w: service.get_default_model
|
||||
});
|
||||
if ( ! service.get_default_model ) {
|
||||
throw new Error('could not infer model from service');
|
||||
}
|
||||
|
@ -16,8 +16,6 @@ const PUTER_PROMPT = `
|
||||
user of the driver interface (typically an app on Puter):
|
||||
`.replace('\n', ' ').trim();
|
||||
|
||||
// Maximum number of input tokens allowed for Claude API requests
|
||||
const MAX_CLAUDE_INPUT_TOKENS = 10000;
|
||||
|
||||
|
||||
/**
|
||||
@ -97,7 +95,6 @@ class ClaudeService extends BaseService {
|
||||
* @param {boolean} options.stream - Whether to stream the response
|
||||
* @param {string} [options.model] - The Claude model to use, defaults to service default
|
||||
* @returns {TypedValue|Object} Returns either a TypedValue with streaming response or a completion object
|
||||
* @throws {APIError} If input token count exceeds maximum allowed
|
||||
*/
|
||||
async complete ({ messages, stream, model }) {
|
||||
const adapted_messages = [];
|
||||
@ -129,33 +126,11 @@ class ClaudeService extends BaseService {
|
||||
adapted_messages.push(message);
|
||||
if ( message.role === 'user' ) {
|
||||
previous_was_user = true;
|
||||
} else {
|
||||
previous_was_user = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the approximate token count for the input messages
|
||||
* @private
|
||||
* @returns {number} Estimated token count based on character length divided by 4
|
||||
* @description Uses a simple character length based heuristic to estimate tokens.
|
||||
* While not perfectly accurate, this provides a reasonable approximation for
|
||||
* checking against max token limits before sending to Claude API.
|
||||
*/
|
||||
const token_count = (() => {
|
||||
const text = JSON.stringify(adapted_messages) +
|
||||
JSON.stringify(system_prompts);
|
||||
|
||||
// This is the most accurate token counter available for Claude.
|
||||
return text.length / 4;
|
||||
})();
|
||||
|
||||
if ( token_count > MAX_CLAUDE_INPUT_TOKENS ) {
|
||||
throw APIError.create('max_tokens_exceeded', null, {
|
||||
input_tokens: token_count,
|
||||
max_tokens: MAX_CLAUDE_INPUT_TOKENS,
|
||||
});
|
||||
}
|
||||
|
||||
if ( stream ) {
|
||||
let usage_promise = new TeePromise();
|
||||
|
||||
@ -168,7 +143,7 @@ class ClaudeService extends BaseService {
|
||||
(async () => {
|
||||
const completion = await this.anthropic.messages.stream({
|
||||
model: model ?? this.get_default_model(),
|
||||
max_tokens: 8096,
|
||||
max_tokens: (model === 'claude-3-5-sonnet-20241022' || model === 'claude-3-5-sonnet-20240620') ? 8192 : 4096,
|
||||
temperature: 0,
|
||||
system: PUTER_PROMPT + JSON.stringify(system_prompts),
|
||||
messages: adapted_messages,
|
||||
@ -205,7 +180,7 @@ class ClaudeService extends BaseService {
|
||||
|
||||
const msg = await this.anthropic.messages.create({
|
||||
model: model ?? this.get_default_model(),
|
||||
max_tokens: 8096,
|
||||
max_tokens: (model === 'claude-3-5-sonnet-20241022' || model === 'claude-3-5-sonnet-20240620') ? 8192 : 4096,
|
||||
temperature: 0,
|
||||
system: PUTER_PROMPT + JSON.stringify(system_prompts),
|
||||
messages: adapted_messages,
|
||||
|
@ -341,8 +341,9 @@ class OpenAICompletionService extends BaseService {
|
||||
const max_tokens = 4096 - token_count;
|
||||
console.log('MAX TOKENS ???', max_tokens);
|
||||
|
||||
const svc_apiErrpr = this.services.get('api-error');
|
||||
if ( max_tokens <= 8 ) {
|
||||
throw APIError.create('max_tokens_exceeded', null, {
|
||||
throw svc_apiErrpr.create('max_tokens_exceeded', {
|
||||
input_tokens: token_count,
|
||||
max_tokens: 4096 - 8,
|
||||
});
|
||||
|
77
src/backend/src/modules/puterai/lib/AsModeration.js
Normal file
77
src/backend/src/modules/puterai/lib/AsModeration.js
Normal file
@ -0,0 +1,77 @@
|
||||
const { default: dedent } = require("dedent");
|
||||
|
||||
class AsModeration {
|
||||
constructor ({ chat, model }) {
|
||||
this.chat = chat;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
async moderate (text) {
|
||||
// copied from Anthropic documentation
|
||||
// https://docs.anthropic.com/en/docs/about-claude/use-case-guides/content-moderation
|
||||
const unsafe_categories = [
|
||||
'Child Exploitation',
|
||||
'Conspiracy Theories',
|
||||
'Hate',
|
||||
'Indiscriminate Weapons',
|
||||
'Intellectual Property',
|
||||
'Non-Violent Crimes',
|
||||
'Privacy',
|
||||
'Self-Harm',
|
||||
'Sex Crimes',
|
||||
'Sexual Content',
|
||||
'Specialized Advice',
|
||||
'Violent Crimes'
|
||||
];
|
||||
|
||||
const unsafe_category_str = unsafe_categories.join('\n');
|
||||
|
||||
const assessment_prompt = dedent(`
|
||||
Determine whether the following message warrants moderation,
|
||||
based on the unsafe categories outlined below.
|
||||
|
||||
Message:
|
||||
<message>
|
||||
${text
|
||||
.replace('<', '<')
|
||||
.replace('>', '>')
|
||||
}
|
||||
</message>
|
||||
|
||||
Unsafe Categories:
|
||||
<categories>
|
||||
${unsafe_category_str}
|
||||
</categories>
|
||||
|
||||
Respond with ONLY a JSON object, using the format below:
|
||||
{{
|
||||
"violation": <Boolean field denoting whether the message should be moderated>,
|
||||
"categories": [Comma-separated list of violated categories],
|
||||
"explanation": [Optional. Only include if there is a violation.]
|
||||
}}
|
||||
`);
|
||||
|
||||
const result = await this.chat.complete({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: assessment_prompt,
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
console.log('result???', require('util').inspect(result, { depth: null }));
|
||||
|
||||
const str = result.message?.content?.[0]?.text ??
|
||||
result.messages?.[0]?.content?.[0]?.text ??
|
||||
'{ "violation": true }';
|
||||
|
||||
const parsed = JSON.parse(str);
|
||||
console.log('parsed?', parsed);
|
||||
return ! parsed.violation;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AsModeration,
|
||||
};
|
@ -17,16 +17,11 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { DB_READ } = require("../../services/database/consts");
|
||||
const { abtest } = require("../../util/otelutil");
|
||||
const { NodePathSelector, NodeUIDSelector, NodeInternalIDSelector, NodeChildSelector, RootNodeSelector } = require("../node/selectors");
|
||||
|
||||
module.exports = class DatabaseFSEntryFetcher {
|
||||
constructor ({ services }) {
|
||||
this.services = services;
|
||||
this.log = services.get('log-service').create('DatabaseFSEntryFetcher');
|
||||
|
||||
this.db = services.get('database').get(DB_READ, 'filesystem');
|
||||
const { NodePathSelector, NodeUIDSelector, NodeInternalIDSelector, NodeChildSelector, RootNodeSelector } = require("../../filesystem/node/selectors");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
|
||||
module.exports = class DatabaseFSEntryFetcher extends BaseService {
|
||||
_construct () {
|
||||
this.defaultProperties = [
|
||||
'id',
|
||||
'associated_app_id',
|
||||
@ -57,6 +52,10 @@ module.exports = class DatabaseFSEntryFetcher {
|
||||
]
|
||||
}
|
||||
|
||||
_init () {
|
||||
this.db = this.services.get('database').get(DB_READ, 'filesystem');
|
||||
}
|
||||
|
||||
async find (selector, fetch_entry_options) {
|
||||
if ( selector instanceof RootNodeSelector ) {
|
||||
return selector.entry;
|
@ -15,6 +15,9 @@ class PuterFSModule extends AdvancedBase {
|
||||
|
||||
const { MountpointService } = require('./MountpointService');
|
||||
services.registerService('mountpoint', MountpointService);
|
||||
|
||||
const DatabaseFSEntryFetcher = require("./DatabaseFSEntryFetcher");
|
||||
services.registerService('fsEntryFetcher', DatabaseFSEntryFetcher);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,9 @@ class SelfHostedModule extends AdvancedBase {
|
||||
|
||||
const DevWatcherService = require('./DevWatcherService');
|
||||
const path_ = require('path');
|
||||
|
||||
const { DBKVService } = require("../../services/DBKVService");
|
||||
services.registerService('puter-kvstore', DBKVService);
|
||||
|
||||
// TODO: sucks
|
||||
const RELATIVE_PATH = '../../../../../';
|
||||
|
@ -16,9 +16,6 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { DBKVStore } = require("../../drivers/DBKVStore");
|
||||
const { EntityStoreImplementation } = require("../../drivers/EntityStoreImplementation");
|
||||
const { HelloWorld } = require("../../drivers/HelloWorld");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
|
||||
class SelfhostedService extends BaseService {
|
||||
@ -27,12 +24,6 @@ class SelfhostedService extends BaseService {
|
||||
`
|
||||
|
||||
async _init () {
|
||||
const svc_driver = this.services.get('driver');
|
||||
|
||||
svc_driver.register_driver('puter-kvstore', new DBKVStore());
|
||||
svc_driver.register_driver('puter-apps', new EntityStoreImplementation({ service: 'es:app' }));
|
||||
svc_driver.register_driver('puter-subdomains', new EntityStoreImplementation({ service: 'es:subdomain' }));
|
||||
svc_driver.register_driver('puter-notifications', new EntityStoreImplementation({ service: 'es:notification' }));
|
||||
}
|
||||
}
|
||||
|
||||
|
54
src/backend/src/modules/web/APIErrorService.js
Normal file
54
src/backend/src/modules/web/APIErrorService.js
Normal file
@ -0,0 +1,54 @@
|
||||
const APIError = require("../../api/APIError");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
|
||||
/**
|
||||
* @typedef {Object} ErrorSpec
|
||||
* @property {string} code - The error code
|
||||
* @property {string} status - HTTP status code
|
||||
* @property {function} message - A function that generates an error message
|
||||
*/
|
||||
|
||||
/**
|
||||
* The APIErrorService class provides a mechanism for registering and managing
|
||||
* error codes and messages which may be sent to clients.
|
||||
*
|
||||
* This allows for a single source-of-truth for error codes and messages that
|
||||
* are used by multiple services.
|
||||
*/
|
||||
class APIErrorService extends BaseService {
|
||||
_construct () {
|
||||
this.codes = {
|
||||
...this.constructor.codes,
|
||||
};
|
||||
}
|
||||
|
||||
// Hardcoded error codes from before this service was created
|
||||
static codes = APIError.codes;
|
||||
|
||||
/**
|
||||
* Registers API error codes.
|
||||
*
|
||||
* @param {Object.<string, ErrorSpec>} codes - A map of error codes to error specifications
|
||||
*/
|
||||
register (codes) {
|
||||
for ( const code in codes ) {
|
||||
this.codes[code] = codes[code];
|
||||
}
|
||||
}
|
||||
|
||||
create (code, fields) {
|
||||
const error_spec = this.codes[code];
|
||||
if ( ! error_spec ) {
|
||||
return new APIError(500, 'Missing error message.', null, {
|
||||
code,
|
||||
});
|
||||
}
|
||||
|
||||
return new APIError(error_spec.status, error_spec.message, null, {
|
||||
...fields,
|
||||
code,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = APIErrorService;
|
@ -19,6 +19,9 @@ class WebModule extends AdvancedBase {
|
||||
|
||||
const WebServerService = require("./WebServerService");
|
||||
services.registerService('web-server', WebServerService);
|
||||
|
||||
const APIErrorService = require("./APIErrorService");
|
||||
services.registerService('api-error', APIErrorService);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +68,8 @@ class WebServerService extends BaseService {
|
||||
router_webhooks: this.router_webhooks,
|
||||
});
|
||||
await services.emit('install.routes-gui', { app });
|
||||
|
||||
this.log.noticeme('web server setup done');
|
||||
}
|
||||
|
||||
|
||||
@ -233,6 +235,7 @@ class WebServerService extends BaseService {
|
||||
try {
|
||||
let auth_res = await jwt_auth(socket);
|
||||
// successful auth
|
||||
socket.actor = auth_res.actor;
|
||||
socket.user = auth_res.user;
|
||||
socket.token = auth_res.token;
|
||||
// join user room
|
||||
@ -249,6 +252,7 @@ class WebServerService extends BaseService {
|
||||
}
|
||||
});
|
||||
|
||||
const context = Context.get();
|
||||
socketio.on('connection', (socket) => {
|
||||
/**
|
||||
* Starts the web server and associated services.
|
||||
@ -266,10 +270,14 @@ class WebServerService extends BaseService {
|
||||
socket.on('trash.is_empty', (msg) => {
|
||||
socket.broadcast.to(socket.user.id).emit('trash.is_empty', msg);
|
||||
});
|
||||
socket.on('puter_is_actually_open', (msg) => {
|
||||
socket.on('puter_is_actually_open', async (msg) => {
|
||||
const svc_event = this.services.get('event');
|
||||
svc_event.emit('web.socket.user-connected', {
|
||||
user: socket.user
|
||||
await context.sub({
|
||||
actor: socket.actor,
|
||||
}).arun(async () => {
|
||||
await svc_event.emit('web.socket.user-connected', {
|
||||
user: socket.user
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -581,8 +589,6 @@ class WebServerService extends BaseService {
|
||||
app.options('/*', (_, res) => {
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
console.log('WEB SERVER INIT DONE');
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
|
@ -26,9 +26,6 @@ const APIError = require('../../../api/APIError.js');
|
||||
* Since Express 5 is not yet released, this function is used by
|
||||
* eggspress() to handle errors instead of as a middleware.
|
||||
*
|
||||
* @todo remove this function and use express error handling
|
||||
* when Express 5 is released
|
||||
*
|
||||
* @param {*} err
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
|
@ -17,7 +17,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const APIError = require('../../../api/APIError.js');
|
||||
const { relativeSelector, NodeUIDSelector } = require('../../../filesystem/node/selectors.js');
|
||||
const { relativeSelector } = require('../../../filesystem/node/selectors.js');
|
||||
const ERR_INVALID_PATHREF = 'Invalid path reference in path: ';
|
||||
const ERR_UNKNOWN_PATHREF = 'Unknown path reference in path: ';
|
||||
|
||||
|
@ -19,18 +19,15 @@
|
||||
const APIError = require("../../../api/APIError");
|
||||
const eggspress = require("../../../api/eggspress");
|
||||
const config = require("../../../config");
|
||||
const PathResolver = require("./PathResolver");
|
||||
const { Context } = require("../../../util/context");
|
||||
const Busboy = require('busboy');
|
||||
const { BatchExecutor } = require("../../../filesystem/batch/BatchExecutor");
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
const { EWMA, MovingMode } = require("../../../util/opmath");
|
||||
const { MovingMode } = require("../../../util/opmath");
|
||||
const { get_app } = require('../../../helpers');
|
||||
const { valid_file_size } = require("../../../util/validutil");
|
||||
const { OnlyOnceFn } = require("../../../util/fnutil.js");
|
||||
|
||||
const commands = require('../../../filesystem/batch/commands.js').commands;
|
||||
|
||||
module.exports = eggspress('/batch', {
|
||||
subdomain: 'api',
|
||||
verified: true,
|
||||
@ -137,8 +134,6 @@ module.exports = eggspress('/batch', {
|
||||
const pending_operations = [];
|
||||
const response_promises = [];
|
||||
const fileinfos = [];
|
||||
let total = 0;
|
||||
let total_tbd = true;
|
||||
|
||||
const on_nonfile_data_end = OnlyOnceFn(() => {
|
||||
if ( request_error ) {
|
||||
@ -223,10 +218,6 @@ module.exports = eggspress('/batch', {
|
||||
}
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
let ended = [];
|
||||
let ps = [];
|
||||
|
||||
busboy.on('file', async (fieldname, stream, detais) => {
|
||||
if ( batch_exe.total_tbd ) {
|
||||
batch_exe.total_tbd = false;
|
||||
|
@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
@ -19,12 +19,8 @@
|
||||
"use strict"
|
||||
const eggspress = require('../../api/eggspress.js');
|
||||
const FSNodeParam = require('../../api/filesystem/FSNodeParam.js');
|
||||
const _path = require('path');
|
||||
const { NodeUIDSelector } = require('../../filesystem/node/selectors.js');
|
||||
const { HLCopy } = require('../../filesystem/hl_operations/hl_copy.js');
|
||||
const { Context } = require('../../util/context.js');
|
||||
const { DatabaseFSEntryService } = require('../../modules/puterfs/DatabaseFSEntryService.js');
|
||||
const { ProxyContainer } = require('../../services/Container.js');
|
||||
|
||||
// -----------------------------------------------------------------------//
|
||||
// POST /copy
|
||||
@ -40,16 +36,12 @@ module.exports = eggspress('/copy', {
|
||||
source: new FSNodeParam('source'),
|
||||
destination: new FSNodeParam('destination'),
|
||||
}
|
||||
}, async (req, res, next) => {
|
||||
}, async (req, res) => {
|
||||
const user = req.user
|
||||
const dedupe_name =
|
||||
req.body.dedupe_name ??
|
||||
req.body.change_name ?? false;
|
||||
|
||||
// // check if source would be an ancestor of destination
|
||||
// if((abs_dest_path + '/').startsWith(abs_source_path + '/')){
|
||||
// return res.status(400).send('Can not copy a item into itself.')
|
||||
// }
|
||||
let frame;
|
||||
{
|
||||
const x = Context.get();
|
||||
@ -66,23 +58,8 @@ module.exports = eggspress('/copy', {
|
||||
x.set(operationTraceSvc.ckey('frame'), frame);
|
||||
}
|
||||
|
||||
const log = req.services.get('log-service').create('copy');
|
||||
const filesystem = req.services.get('filesystem');
|
||||
|
||||
// copy
|
||||
const {get_app, uuid2fsentry, is_shared_with_anyone, suggest_app_for_fsentry} = require('../../helpers.js')
|
||||
let new_fsentries = [];
|
||||
|
||||
const tracer = req.services.get('traceService').tracer;
|
||||
await tracer.startActiveSpan('filesystem_api.copy', async span => {
|
||||
// const op = await filesystem.cp(req.fs, {
|
||||
// source: req.values.source,
|
||||
// destinationOrParent: req.values.destination,
|
||||
// user: user,
|
||||
// new_name: req.body.new_name,
|
||||
// overwrite: req.body.overwrite ?? false,
|
||||
// dedupe_name,
|
||||
// });
|
||||
|
||||
// === upcoming copy behaviour ===
|
||||
const hl_copy = new HLCopy();
|
||||
|
@ -50,11 +50,11 @@ module.exports = eggspress('/delete', {
|
||||
|
||||
// try to delete each path in the array one by one (if glob, resolve first)
|
||||
// TODO: remove this pseudo-batch
|
||||
for(let j=0; j < paths.length; j++){
|
||||
let item_path = paths[j];
|
||||
for ( const item_path of paths ) {
|
||||
let item_path = item_path;
|
||||
const target = await (new FSNodeParam('path')).consolidate({
|
||||
req: { fs: req.fs, user },
|
||||
getParam: () => paths[j],
|
||||
getParam: () => item_path,
|
||||
});
|
||||
const hl_remove = new HLRemove();
|
||||
await hl_remove.run({
|
||||
|
@ -17,16 +17,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"use strict"
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../middleware/auth.js');
|
||||
const config = require('../../config.js');
|
||||
const PerformanceMonitor = require('../../monitor/PerformanceMonitor.js');
|
||||
const { Context } = require('../../util/context.js');
|
||||
const eggspress = require('../../api/eggspress.js');
|
||||
const FSNodeParam = require('../../api/filesystem/FSNodeParam.js');
|
||||
const FlagParam = require('../../api/filesystem/FlagParam.js');
|
||||
const { LLReadDir } = require('../../filesystem/ll_operations/ll_readdir.js');
|
||||
const { HLReadDir } = require('../../filesystem/hl_operations/hl_readdir.js');
|
||||
|
||||
// -----------------------------------------------------------------------//
|
||||
@ -47,35 +41,12 @@ module.exports = eggspress('/readdir', {
|
||||
no_assocs: new FlagParam('no_assocs', { optional: true }),
|
||||
}
|
||||
}, async (req, res, next) => {
|
||||
const monitor = PerformanceMonitor.createContext("router.readdir");
|
||||
|
||||
let log; {
|
||||
const x = Context.get();
|
||||
log = x.get('services').get('log-service').create('readdir');
|
||||
log.info(`readdir: ${req.body.path}`);
|
||||
}
|
||||
|
||||
// // `path` validation
|
||||
// if(req.body.path === undefined)
|
||||
// return res.status(400).send('path is required.')
|
||||
// else if(req.body.path === '')
|
||||
// return res.status(400).send('path cannot be empty.')
|
||||
// else if(req.body.path === null)
|
||||
// return res.status(400).send('path cannot be null.')
|
||||
// else if(typeof req.body.path !== 'string')
|
||||
// return res.status(400).send('path must be a string.')
|
||||
|
||||
// if ( req.body.path.startsWith('~') ) {
|
||||
// const homedir = `/${req.user.username}`;
|
||||
// req.body.path = homedir + req.body.path.slice(1);
|
||||
// }
|
||||
|
||||
// `recursive` validation
|
||||
// if(req.body.recursive !== undefined && typeof req.body.recursive !== 'boolean')
|
||||
// return res.status(400).send('recursive must be a boolean.')
|
||||
// else if(req.body.recursive === undefined)
|
||||
// req.body.recursive = false; // default value
|
||||
|
||||
const subject = req.values.subject;
|
||||
const recursive = req.values.recursive;
|
||||
const no_thumbs = req.values.no_thumbs;
|
||||
|
@ -56,7 +56,7 @@ module.exports = eggspress('/rename', {
|
||||
// modules
|
||||
const db = req.services.get('database').get(DB_WRITE, 'filesystem');
|
||||
const mime = require('mime-types');
|
||||
const {get_app, validate_fsentry_name, uuid2fsentry, chkperm, id2path} = require('../../helpers.js');
|
||||
const {get_app, validate_fsentry_name, id2path} = require('../../helpers.js');
|
||||
const _path = require('path');
|
||||
|
||||
// new_name validation
|
||||
@ -168,7 +168,7 @@ module.exports = eggspress('/rename', {
|
||||
is_dir: fsentry.is_dir,
|
||||
path: new_path,
|
||||
old_path: old_path,
|
||||
type: contentType ? contentType : null,
|
||||
type: contentType || null,
|
||||
associated_app: associated_app,
|
||||
original_client_socket_id: req.body.original_client_socket_id,
|
||||
};
|
||||
|
@ -1,7 +1,5 @@
|
||||
const eggspress = require("../../api/eggspress");
|
||||
const { HLNameSearch } = require("../../filesystem/hl_operations/hl_name_search");
|
||||
const { subdomain } = require("../../helpers");
|
||||
const verified = require("../../middleware/verified");
|
||||
|
||||
module.exports = eggspress('/search', {
|
||||
subdomain: 'api',
|
||||
|
@ -53,10 +53,7 @@ module.exports = eggspress(['/up', '/write'], {
|
||||
};
|
||||
|
||||
// modules
|
||||
const {get_app, mkdir} = require('../../helpers.js')
|
||||
|
||||
// if(!req.files)
|
||||
// return res.status(400).send('No files uploaded');
|
||||
const {get_app} = require('../../helpers.js')
|
||||
|
||||
// Is this an entry for an app?
|
||||
let app;
|
||||
@ -89,13 +86,6 @@ module.exports = eggspress(['/up', '/write'], {
|
||||
x.set(svc_clientOperation.ckey('tracker'), tracker);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------
|
||||
// Variables used by busboy callbacks
|
||||
//-------------------------------------------------------------
|
||||
const on_first_file = () => {
|
||||
frame_meta_ready();
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------
|
||||
// Multipart processing (using busboy)
|
||||
//-------------------------------------------------------------
|
||||
@ -205,37 +195,4 @@ module.exports = eggspress(['/up', '/write'], {
|
||||
|
||||
if ( frame ) frame.done();
|
||||
return res.send(response);
|
||||
|
||||
// upload files one by one
|
||||
// for (let index = 0; index < req.files.length; index++) {
|
||||
// let uploaded_file = req.files[index];
|
||||
|
||||
// // TEMP: create stream from buffer
|
||||
// if ( uploaded_file.buffer ) {
|
||||
// uploaded_file = { ...uploaded_file };
|
||||
// const buffer = uploaded_file.buffer;
|
||||
// uploaded_file.stream = (() => {
|
||||
// const { Readable } = require('stream');
|
||||
// return Readable.from(buffer);
|
||||
// })();
|
||||
// delete uploaded_file.buffer;
|
||||
// }
|
||||
|
||||
// const hl_write = new HLWrite();
|
||||
// const response = await hl_write.run({
|
||||
// destination_or_parent: req.values.fsNode,
|
||||
// specified_name: req.body.name,
|
||||
// fallback_name: uploaded_file.originalname,
|
||||
// overwrite: await boolify(req.body.overwrite),
|
||||
// dedupe_name: await boolify(req.body.dedupe_name),
|
||||
// shortcut_to: req.values.target,
|
||||
|
||||
// create_missing_parents: boolify(req.body.create_missing_ancestors),
|
||||
|
||||
// user: req.user,
|
||||
// file: uploaded_file,
|
||||
// });
|
||||
|
||||
// return res.send(response);
|
||||
// }
|
||||
});
|
||||
|
@ -27,8 +27,8 @@ const { DB_READ } = require('../services/database/consts.js');
|
||||
// GET /get-launch-apps
|
||||
// -----------------------------------------------------------------------//
|
||||
router.get('/get-launch-apps', auth, express.json(), async (req, res, next)=>{
|
||||
let final_returned_obj = {};
|
||||
let retobj = [];
|
||||
let result = {};
|
||||
|
||||
// -----------------------------------------------------------------------//
|
||||
// Recent apps
|
||||
// -----------------------------------------------------------------------//
|
||||
@ -45,37 +45,35 @@ router.get('/get-launch-apps', auth, express.json(), async (req, res, next)=>{
|
||||
'SELECT DISTINCT app_uid FROM app_opens WHERE user_id = ? GROUP BY app_uid ORDER BY MAX(_id) DESC LIMIT 10',
|
||||
[req.user.id]);
|
||||
// Update cache with the results from the db (if any results were returned)
|
||||
if(apps && Array.isArray(apps) && apps.length > 0)
|
||||
if(apps && Array.isArray(apps) && apps.length > 0) {
|
||||
kv.set('app_opens:user:' + req.user.id, apps);
|
||||
}
|
||||
|
||||
for (let index = 0; index < apps.length; index++) {
|
||||
const app = await get_app({uid: apps[index].app_uid});
|
||||
let final_obj = {};
|
||||
|
||||
// prepare each app for returning to user by only returning the necessary fields
|
||||
// and adding them to the retobj array
|
||||
if(app){
|
||||
final_obj = {
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
};
|
||||
}
|
||||
// add to object to be returned
|
||||
retobj.push(final_obj)
|
||||
}
|
||||
final_returned_obj.recent = retobj;
|
||||
|
||||
// prepare each app for returning to user by only returning the necessary fields
|
||||
// and adding them to the retobj array
|
||||
result.recent = [];
|
||||
console.log('\x1B[36;1m -------- RECENT APPS -------- \x1B[0m', apps);
|
||||
for ( const { app_uid: uid } of apps ) {
|
||||
console.log('\x1B[36;1m -------- UID -------- \x1B[0m', uid);
|
||||
const app = await get_app({ uid });
|
||||
if ( ! app ) continue
|
||||
|
||||
result.recent.push({
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------//
|
||||
// Recommended apps
|
||||
// -----------------------------------------------------------------------//
|
||||
// reset retobj
|
||||
retobj = [];
|
||||
let app_names = [
|
||||
let app_names = new Set([
|
||||
'app-center',
|
||||
'dev-center',
|
||||
'editor',
|
||||
@ -99,49 +97,27 @@ router.get('/get-launch-apps', auth, express.json(), async (req, res, next)=>{
|
||||
'plushie-connect',
|
||||
'hex-frvr',
|
||||
'spider-solitaire',
|
||||
]
|
||||
]);
|
||||
|
||||
// Prepare each app for returning to user by only returning the necessary fields
|
||||
// and adding them to the retobj array
|
||||
if(app_names.length > 0){
|
||||
for (let index = 0; index < app_names.length; index++) {
|
||||
const app = await get_app({name: app_names[index]});
|
||||
result.recommended = [];
|
||||
for ( const name of app_names ) {
|
||||
const app = await get_app({ name });
|
||||
if ( ! app ) continue;
|
||||
|
||||
let final_obj = {};
|
||||
if(app){
|
||||
final_obj = {
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
};
|
||||
}
|
||||
// add to object to be returned
|
||||
retobj.push(final_obj)
|
||||
}
|
||||
|
||||
// remove duplicates from retobj
|
||||
if(retobj.length > 0)
|
||||
retobj = retobj.filter((obj, pos, arr) => {
|
||||
return arr.map(mapObj => mapObj['name']).indexOf(obj['name']) === pos;
|
||||
})
|
||||
result.recommended.push({
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
});
|
||||
}
|
||||
|
||||
// Order output based on input!
|
||||
let final_obj = [];
|
||||
for (let index = 0; index < app_names.length; index++) {
|
||||
const app_name = app_names[index];
|
||||
for (let index = 0; index < retobj.length; index++) {
|
||||
if(retobj[index].name === app_name)
|
||||
final_obj.push(retobj[index]);
|
||||
}
|
||||
}
|
||||
|
||||
final_returned_obj.recommended = final_obj;
|
||||
|
||||
return res.send(final_returned_obj);
|
||||
return res.send(result);
|
||||
})
|
||||
module.exports = router
|
||||
|
||||
module.exports = router
|
||||
|
@ -17,11 +17,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"use strict"
|
||||
const {get_taskbar_items, generate_system_fsentries, send_email_verification_code, send_email_verification_token, username_exists, invalidate_cached_user_by_id, get_user } = require('../helpers');
|
||||
const {get_taskbar_items, send_email_verification_code, send_email_verification_token, username_exists, invalidate_cached_user_by_id, get_user } = require('../helpers');
|
||||
const config = require('../config');
|
||||
const eggspress = require('../api/eggspress');
|
||||
const { Context } = require('../util/context');
|
||||
const { DB_WRITE } = require('../services/database/consts');
|
||||
const { generate_identifier } = require('../util/identifier');
|
||||
|
||||
async function generate_random_username () {
|
||||
let username;
|
||||
|
@ -210,12 +210,13 @@ class Container {
|
||||
*/
|
||||
async emit (id, ...args) {
|
||||
if ( this.logger ) {
|
||||
this.logger.noticeme(`services:event ${id}`, { args });
|
||||
this.logger.info(`services:event ${id}`, { args });
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
for ( const k in this.instances_ ) {
|
||||
if ( this.instances_[k].__on ) {
|
||||
promises.push(this.instances_[k].__on(id, args));
|
||||
promises.push(Context.arun(() => this.instances_[k].__on(id, args)));
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
197
src/backend/src/services/DBKVService.js
Normal file
197
src/backend/src/services/DBKVService.js
Normal file
@ -0,0 +1,197 @@
|
||||
const { get_app } = require("../helpers");
|
||||
const { Context } = require("../util/context");
|
||||
const BaseService = require("./BaseService");
|
||||
const { DB_READ } = require("./database/consts");
|
||||
|
||||
class DBKVService extends BaseService {
|
||||
static MODULES = {
|
||||
murmurhash: require('murmurhash'),
|
||||
}
|
||||
|
||||
_init () {
|
||||
this.db = this.services.get('database').get(DB_READ, 'kvstore');
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['puter-kvstore']: {
|
||||
async get ({ app_uid, key }) {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
// If the actor is an app then it gets its own KV store.
|
||||
// The way this is implemented isn't ideal for future behaviour;
|
||||
// a KV implementation specified by the user would have parameters
|
||||
// that are scoped to the app, so this should eventually be
|
||||
// changed to get the app ID from the same interface that would
|
||||
// be used to obtain per-app user-specified implementation params.
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
const kv = app ? await this.db.read(
|
||||
`SELECT * FROM kv WHERE user_id=? AND app=? AND kkey_hash=? LIMIT 1`,
|
||||
[ user.id, app.uid, key_hash ]
|
||||
) : await this.db.read(
|
||||
`SELECT * FROM kv WHERE user_id=? AND (app IS NULL OR app = 'global') AND kkey_hash=? LIMIT 1`,
|
||||
[ user.id, key_hash ]
|
||||
);
|
||||
|
||||
if ( kv[0] ) kv[0].value = this.db.case({
|
||||
mysql: () => kv[0].value,
|
||||
otherwise: () => JSON.parse(kv[0].value ?? 'null'),
|
||||
})();
|
||||
|
||||
return kv[0]?.value ?? null;
|
||||
},
|
||||
async set ({ app_uid, key, value }) {
|
||||
const actor = Context.get('actor');
|
||||
const config = this.global_config;
|
||||
|
||||
// Validate the key
|
||||
// get() doesn't String() the key but it only passes it to
|
||||
// murmurhash.v3() so it doesn't need to ¯\_(ツ)_/¯
|
||||
key = String(key);
|
||||
if ( Buffer.byteLength(key, 'utf8') > config.kv_max_key_size ) {
|
||||
throw new Error(`key is too large. Max size is ${config.kv_max_key_size}.`);
|
||||
}
|
||||
|
||||
// Validate the value
|
||||
value = value === undefined ? null : value;
|
||||
if (
|
||||
value !== null &&
|
||||
Buffer.byteLength(JSON.stringify(value), 'utf8') >
|
||||
config.kv_max_value_size
|
||||
) {
|
||||
throw new Error(`value is too large. Max size is ${config.kv_max_value_size}.`);
|
||||
}
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
|
||||
try {
|
||||
await this.db.write(
|
||||
`INSERT INTO kv (user_id, app, kkey_hash, kkey, value)
|
||||
VALUES (?, ?, ?, ?, ?) ` +
|
||||
this.db.case({
|
||||
mysql: 'ON DUPLICATE KEY UPDATE value = ?',
|
||||
sqlite: 'ON CONFLICT(user_id, app, kkey_hash) DO UPDATE SET value = excluded.value',
|
||||
}),
|
||||
[
|
||||
user.id, app?.uid ?? 'global', key_hash, key,
|
||||
JSON.stringify(value),
|
||||
...this.db.case({ mysql: [value], otherwise: [] }),
|
||||
]
|
||||
);
|
||||
} catch (e) {
|
||||
// I discovered that my .sqlite file was corrupted and the update
|
||||
// above didn't work. The current database initialization does not
|
||||
// cause this issue so I'm adding this log as a safeguard.
|
||||
// - KernelDeimos / ED
|
||||
const svc_error = this.services.get('error-service');
|
||||
svc_error.report('kvstore:sqlite_error', {
|
||||
message: 'Broken database version - please contact maintainers',
|
||||
source: e,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async del ({ app_uid, key }) {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
|
||||
await this.db.write(
|
||||
`DELETE FROM kv WHERE user_id=? AND app=? AND kkey_hash=?`,
|
||||
[ user.id, app?.uid ?? 'global', key_hash ]
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
async list ({ app_uid, as }) {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
let rows = app ? await this.db.read(
|
||||
`SELECT kkey, value FROM kv WHERE user_id=? AND app=?`,
|
||||
[ user.id, app.uid ]
|
||||
) : await this.db.read(
|
||||
`SELECT kkey, value FROM kv WHERE user_id=? AND (app IS NULL OR app = 'global')`,
|
||||
[ user.id ]
|
||||
);
|
||||
|
||||
rows = rows.map(row => ({
|
||||
key: row.kkey,
|
||||
value: this.db.case({
|
||||
mysql: () => row.value,
|
||||
otherwise: () => JSON.parse(row.value ?? 'null')
|
||||
})(),
|
||||
}));
|
||||
|
||||
as = as || 'entries';
|
||||
|
||||
if ( ! ['keys','values','entries'].includes(as) ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'as',
|
||||
expected: '"keys", "values", or "entries"',
|
||||
});
|
||||
}
|
||||
|
||||
if ( as === 'keys' ) rows = rows.map(row => row.key);
|
||||
else if ( as === 'values' ) rows = rows.map(row => row.value);
|
||||
|
||||
return rows;
|
||||
},
|
||||
async flush ({ app_uid }) {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
}
|
||||
|
||||
await this.db.write(
|
||||
`DELETE FROM kv WHERE user_id=? AND app=?`,
|
||||
[ user.id, app?.uid ?? 'global' ]
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DBKVService,
|
||||
};
|
@ -18,12 +18,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const APIError = require("../api/APIError");
|
||||
const { Entity } = require("../om/entitystorage/Entity");
|
||||
const { IdentifierUtil } = require("../om/IdentifierUtil");
|
||||
const { Null } = require("../om/query/query");
|
||||
const { Null, And, Eq } = require("../om/query/query");
|
||||
const { Context } = require("../util/context");
|
||||
const BaseService = require("./BaseService");
|
||||
|
||||
|
||||
/**
|
||||
* EntityStoreService - A service class that manages entity-related operations in the backend of Puter.
|
||||
* This class extends BaseService to provide methods for creating, reading, updating, selecting,
|
||||
@ -71,6 +71,70 @@ class EntityStoreService extends BaseService {
|
||||
entity_name: args.entity,
|
||||
});
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['crud-q']: {
|
||||
async create ({ object, options }) {
|
||||
if ( object.hasOwnProperty(this.om.primary_identifier) ) {
|
||||
throw APIError.create('field_not_allowed_for_create', null, {
|
||||
key: this.om.primary_identifier
|
||||
});
|
||||
}
|
||||
const entity = await Entity.create({ om: this.om }, object);
|
||||
return await this.create(entity, options);
|
||||
},
|
||||
async update ({ object, id, options }) {
|
||||
const entity = await Entity.create({ om: this.om }, object);
|
||||
return await this.update(entity, id, options);
|
||||
},
|
||||
async upsert ({ object, id, options }) {
|
||||
const entity = await Entity.create({ om: this.om }, object);
|
||||
return await this.upsert(entity, id, options);
|
||||
},
|
||||
async read ({ uid, id }) {
|
||||
if ( ! uid && ! id ) {
|
||||
throw APIError.create('xor_field_missing', null, {
|
||||
names: ['uid', 'id'],
|
||||
});
|
||||
}
|
||||
|
||||
const entity = await this.fetch_based_on_either_id_(uid, id);
|
||||
if ( ! entity ) {
|
||||
throw APIError.create('entity_not_found', null, {
|
||||
identifier: uid
|
||||
});
|
||||
}
|
||||
return await entity.get_client_safe();
|
||||
},
|
||||
async select (options) {
|
||||
const entities = await this.select(options);
|
||||
const client_safe_entities = [];
|
||||
for ( const entity of entities ) {
|
||||
client_safe_entities.push(await entity.get_client_safe());
|
||||
}
|
||||
return client_safe_entities;
|
||||
},
|
||||
async delete ({ uid, id }) {
|
||||
if ( ! uid && ! id ) {
|
||||
throw APIError.create('xor_field_missing', null, {
|
||||
names: ['uid', 'id'],
|
||||
});
|
||||
}
|
||||
|
||||
if ( id && ! uid ) {
|
||||
const entity = await this.fetch_based_on_complex_id_(id);
|
||||
if ( ! entity ) {
|
||||
throw APIError.create('entity_not_found', null, {
|
||||
identifier: id
|
||||
});
|
||||
}
|
||||
uid = await entity.get(this.om.primary_identifier);
|
||||
}
|
||||
|
||||
return await this.delete(uid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: can replace these with MethodProxyFeature
|
||||
/**
|
||||
@ -214,6 +278,68 @@ class EntityStoreService extends BaseService {
|
||||
}
|
||||
return await this.upstream.delete(uid, { old_entity });
|
||||
}
|
||||
|
||||
async fetch_based_on_complex_id_ (id) {
|
||||
// Ensure `id` is an object and get its keys
|
||||
if ( ! id || typeof id !== 'object' || Array.isArray(id) ) {
|
||||
throw APIError.create('invalid_id', null, { id });
|
||||
}
|
||||
|
||||
const id_keys = Object.keys(id);
|
||||
// sort keys alphabetically
|
||||
id_keys.sort();
|
||||
|
||||
// Ensure key set is valid based on redundant keys listing
|
||||
const redundant_identifiers = this.om.redundant_identifiers ?? [];
|
||||
|
||||
let match_found = false;
|
||||
for ( let key of redundant_identifiers ) {
|
||||
// Either a single key or a list
|
||||
key = Array.isArray(key) ? key : [key];
|
||||
|
||||
// All keys in the list must be present in the id
|
||||
for ( let i=0 ; i < key.length ; i++ ) {
|
||||
if ( ! id_keys.includes(key[i]) ) {
|
||||
break;
|
||||
}
|
||||
if ( i === key.length - 1 ) {
|
||||
match_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! match_found ) {
|
||||
throw APIError.create('invalid_id', null, { id });
|
||||
}
|
||||
|
||||
// Construct a query predicate based on the keys
|
||||
const key_eqs = [];
|
||||
for ( const key of id_keys ) {
|
||||
key_eqs.push(new Eq({
|
||||
key,
|
||||
value: id[key],
|
||||
}));
|
||||
}
|
||||
let predicate = new And({ children: key_eqs });
|
||||
|
||||
// Perform a select
|
||||
const entity = await this.read({ predicate });
|
||||
if ( ! entity ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure there is only one result
|
||||
return entity;
|
||||
}
|
||||
|
||||
async fetch_based_on_either_id_ (uid, id) {
|
||||
if ( uid ) {
|
||||
return await this.read(uid);
|
||||
}
|
||||
|
||||
return await this.fetch_based_on_complex_id_(id);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -19,6 +19,7 @@
|
||||
*/
|
||||
const APIError = require("../api/APIError");
|
||||
const auth2 = require("../middleware/auth2");
|
||||
const { Context } = require("../util/context");
|
||||
const { Endpoint } = require("../util/expressutil");
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
const BaseService = require("./BaseService");
|
||||
|
@ -22,11 +22,12 @@ const APIError = require("../../api/APIError");
|
||||
const { DriverError } = require("./DriverError");
|
||||
const { TypedValue } = require("./meta/Runtime");
|
||||
const BaseService = require("../BaseService");
|
||||
const { Driver } = require("../../definitions/Driver");
|
||||
const { PermissionUtil } = require("../auth/PermissionService");
|
||||
const { Invoker } = require("../../../../putility/src/libs/invoker");
|
||||
const { get_user } = require("../../helpers");
|
||||
|
||||
const strutil = require('@heyputer/putility').libs.string;
|
||||
|
||||
/**
|
||||
* DriverService provides the functionality of Puter drivers.
|
||||
* This class is responsible for managing and interacting with Puter drivers.
|
||||
@ -51,8 +52,52 @@ class DriverService extends BaseService {
|
||||
this.interface_to_test_service = {};
|
||||
this.service_aliases = {};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method is responsible for calling a driver's method with provided arguments.
|
||||
* It checks for permissions, selects the best option, and applies rate and monthly usage limits before invoking the driver.
|
||||
*
|
||||
* @param {Object} o - An object containing driver, interface, method, and arguments.
|
||||
* @returns {Promise<{success: boolean, service: DriverService.Driver, result: any, metadata: any}>}
|
||||
*/
|
||||
_init () {
|
||||
const svc_registry = this.services.get('registry');
|
||||
svc_registry.register_collection('');
|
||||
|
||||
const { quot } = strutil;
|
||||
const svc_apiError = this.services.get('api-error');
|
||||
svc_apiError.register({
|
||||
'missing_required_argument': {
|
||||
status: 400,
|
||||
message: ({ interface_name, method_name, arg_name }) =>
|
||||
`Missing required argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}`,
|
||||
},
|
||||
'argument_consolidation_failed': {
|
||||
status: 400,
|
||||
message: ({ interface_name, method_name, arg_name, message }) =>
|
||||
`Failed to parse or process argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}: ${message}`,
|
||||
},
|
||||
'interface_not_found': {
|
||||
status: 404,
|
||||
message: ({ interface_name }) => `Interface not found: ${quot(interface_name)}`,
|
||||
},
|
||||
'method_not_found': {
|
||||
status: 404,
|
||||
message: ({ interface_name, method_name }) => `Method not found: ${quot(method_name)} on interface ${quot(interface_name)}`,
|
||||
},
|
||||
'no_implementation_available': {
|
||||
status: 502,
|
||||
message: ({
|
||||
iface,
|
||||
interface_name,
|
||||
driver
|
||||
}) => `No implementation available for ` +
|
||||
(iface ?? interface_name) ? 'interface' : 'driver' +
|
||||
' ' + quot(iface ?? interface_name ?? driver) + '.',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for registering collections in the service registry.
|
||||
* It registers 'interfaces', 'drivers', and 'types' collections.
|
||||
@ -92,19 +137,6 @@ class DriverService extends BaseService {
|
||||
{ col_drivers });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method is responsible for calling a driver's method with provided arguments.
|
||||
* It checks for permissions, selects the best option, and applies rate and monthly usage limits before invoking the driver.
|
||||
*
|
||||
* @param {Object} o - An object containing driver, interface, method, and arguments.
|
||||
* @returns {Promise<{success: boolean, service: DriverService.Driver, result: any, metadata: any}>}
|
||||
*/
|
||||
_init () {
|
||||
const svc_registry = this.services.get('registry');
|
||||
svc_registry.register_collection('');
|
||||
}
|
||||
|
||||
register_driver (interface_name, implementation) {
|
||||
this.interface_to_implementation[interface_name] = implementation;
|
||||
}
|
||||
@ -173,7 +205,6 @@ class DriverService extends BaseService {
|
||||
* It returns a promise that resolves to an object containing the result, metadata, and an error if one occurred.
|
||||
*/
|
||||
async _call ({ driver, iface, method, args }) {
|
||||
console.log('??', driver, iface, method, args);
|
||||
const processed_args = await this._process_args(iface, method, args);
|
||||
const test_mode = Context.get('test_mode');
|
||||
if ( test_mode ) {
|
||||
@ -185,13 +216,6 @@ class DriverService extends BaseService {
|
||||
throw Error('actor not found in context');
|
||||
}
|
||||
|
||||
const services = Context.get('services');
|
||||
const svc_permission = services.get('permission');
|
||||
|
||||
|
||||
const svc_registry = this.services.get('registry');
|
||||
const c_interfaces = svc_registry.get('interfaces');
|
||||
|
||||
// There used to be only an 'interface' parameter but no 'driver'
|
||||
// parameter. To support outdated clients we use this hard-coded
|
||||
// table to map interfaces to default drivers.
|
||||
@ -200,9 +224,21 @@ class DriverService extends BaseService {
|
||||
['puter-tts']: 'aws-polly',
|
||||
['puter-chat-completion']: 'openai-completion',
|
||||
['puter-image-generation']: 'openai-image-generation',
|
||||
'puter-apps': 'es:app',
|
||||
'puter-subdomains': 'es:subdomain',
|
||||
'puter-notifications': 'es:notification',
|
||||
}
|
||||
|
||||
driver = driver ?? iface_to_driver[iface] ?? iface;
|
||||
|
||||
// For these ones, the interface specified actually specifies the
|
||||
// specificc driver to use.
|
||||
const iface_to_iface = {
|
||||
'puter-apps': 'crud-q',
|
||||
'puter-subdomains': 'crud-q',
|
||||
'puter-notifications': 'crud-q',
|
||||
}
|
||||
iface = iface_to_iface[iface] ?? iface;
|
||||
|
||||
let skip_usage = false;
|
||||
if ( test_mode && this.interface_to_test_service[iface] ) {
|
||||
@ -226,94 +262,36 @@ class DriverService extends BaseService {
|
||||
* @returns {DriverService} The driver service instance for the provided interface.
|
||||
*/
|
||||
const driver_service_exists = (() => {
|
||||
console.log('CHECKING FOR THIS', driver, iface);
|
||||
return this.services.has(driver) &&
|
||||
this.services.get(driver).list_traits()
|
||||
.includes(iface);
|
||||
})();
|
||||
if ( driver_service_exists ) {
|
||||
const service = this.services.get(driver);
|
||||
|
||||
const caps = service.as('driver-capabilities');
|
||||
if ( test_mode && caps && caps.supports_test_mode(iface, method) ) {
|
||||
skip_usage = true;
|
||||
}
|
||||
|
||||
return await Context.sub({
|
||||
client_driver_call,
|
||||
}).arun(async () => {
|
||||
const result = await this.call_new_({
|
||||
actor,
|
||||
service,
|
||||
service_name: driver,
|
||||
iface, method, args: processed_args,
|
||||
skip_usage,
|
||||
});
|
||||
result.metadata = client_driver_call.response_metadata;
|
||||
return result;
|
||||
if ( ! driver_service_exists ) {
|
||||
const svc_apiError = this.services.get('api-error');
|
||||
throw svc_apiError.create('no_implementation_available', { iface });
|
||||
}
|
||||
|
||||
const service = this.services.get(driver);
|
||||
|
||||
const caps = service.as('driver-capabilities');
|
||||
if ( test_mode && caps && caps.supports_test_mode(iface, method) ) {
|
||||
skip_usage = true;
|
||||
}
|
||||
|
||||
return await Context.sub({
|
||||
client_driver_call,
|
||||
}).arun(async () => {
|
||||
const result = await this.call_new_({
|
||||
actor,
|
||||
service,
|
||||
service_name: driver,
|
||||
iface, method, args: processed_args,
|
||||
skip_usage,
|
||||
});
|
||||
}
|
||||
|
||||
const reading = await svc_permission.scan(actor, `driver:${iface}:${method}`);
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
if ( ! (options.length > 0) ) {
|
||||
throw APIError.create('permission_denied');
|
||||
}
|
||||
|
||||
const instance = this.get_default_implementation(iface);
|
||||
if ( ! instance ) {
|
||||
throw APIError.create('no_implementation_available', null, { iface })
|
||||
}
|
||||
/**
|
||||
* This method is responsible for calling a driver method. It performs various checks and preparations before making the actual call.
|
||||
* It first checks if the driver service exists for the given driver and interface. If it does, it checks if the driver supports the test mode. If it does, it skips the usage of the driver.
|
||||
* If the driver service does not exist, it looks for a default implementation for the given interface and uses it if found.
|
||||
* The method then calls the driver method with the processed arguments and returns the result. If an error occurs during the call, it is caught and handled accordingly.
|
||||
*
|
||||
* @param {Object} o - An object containing the driver name, interface name, method name, and arguments.
|
||||
* @returns {Promise<Object>} - A promise that resolves to an object containing the result of the driver method call, or an error object if an error occurred.
|
||||
*/
|
||||
const meta = await (async () => {
|
||||
if ( instance instanceof Driver ) {
|
||||
return await instance.get_response_meta();
|
||||
}
|
||||
if ( ! instance.instance.as('driver-metadata') ) return;
|
||||
const t = instance.instance.as('driver-metadata');
|
||||
return t.get_response_meta();
|
||||
})();
|
||||
try {
|
||||
let result;
|
||||
if ( instance instanceof Driver ) {
|
||||
result = await instance.call(
|
||||
method, processed_args);
|
||||
} else {
|
||||
// TODO: SLA and monthly limits do not apply do drivers
|
||||
// from service traits (yet)
|
||||
result = await instance.impl[method](processed_args);
|
||||
}
|
||||
if ( result instanceof TypedValue ) {
|
||||
const interface_ = c_interfaces.get(iface);
|
||||
let desired_type = interface_.methods[method]
|
||||
.result_choices[0].type;
|
||||
const svc_coercion = services.get('coercion');
|
||||
result = await svc_coercion.coerce(desired_type, result);
|
||||
// meta.type = result.type.toString(),
|
||||
}
|
||||
return { success: true, ...meta, result };
|
||||
} catch ( e ) {
|
||||
let for_user = (e instanceof APIError) || (e instanceof DriverError);
|
||||
if ( ! for_user ) this.errors.report(`driver:${iface}:${method}`, {
|
||||
source: e,
|
||||
trace: true,
|
||||
// TODO: alarm will not be suitable for all errors.
|
||||
alarm: true,
|
||||
extra: {
|
||||
args,
|
||||
}
|
||||
});
|
||||
this.log.error('Driver error response: ' + e.toString());
|
||||
return this._driver_response_from_error(e, meta);
|
||||
}
|
||||
result.metadata = client_driver_call.response_metadata;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -374,17 +352,12 @@ class DriverService extends BaseService {
|
||||
actor,
|
||||
PermissionUtil.join('service', service_name, 'ii', iface),
|
||||
);
|
||||
console.log({
|
||||
perm: PermissionUtil.join('service', service_name, 'ii', iface),
|
||||
reading,
|
||||
});
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
if ( options.length <= 0 ) {
|
||||
throw APIError.create('forbidden');
|
||||
}
|
||||
const option = await this.select_best_option_(options);
|
||||
const policies = await this.get_policies_for_option_(option);
|
||||
console.log('SLA', JSON.stringify(policies, undefined, ' '));
|
||||
|
||||
// NOT FINAL: For now we apply monthly usage logic
|
||||
// to the first holder of the permission. Later this
|
||||
@ -491,14 +464,12 @@ class DriverService extends BaseService {
|
||||
on_return: async result => {
|
||||
if ( skip_usage ) return result;
|
||||
|
||||
console.log('monthly usage is returning');
|
||||
const svc_monthlyUsage = services.get('monthly-usage');
|
||||
const extra = {
|
||||
'driver.interface': iface,
|
||||
'driver.implementation': service_name,
|
||||
'driver.method': method,
|
||||
};
|
||||
console.log('calling the increment method')
|
||||
await svc_monthlyUsage.increment(actor, method_key, extra);
|
||||
return result;
|
||||
},
|
||||
@ -527,9 +498,7 @@ class DriverService extends BaseService {
|
||||
const svc_registry = this.services.get('registry');
|
||||
const c_interfaces = svc_registry.get('interfaces');
|
||||
|
||||
console.log('????--1', iface);
|
||||
const interface_ = c_interfaces.get(iface);
|
||||
console.log('????--2', interface_);
|
||||
const method_spec = interface_.methods[method];
|
||||
let desired_type =
|
||||
method_spec.result_choices
|
||||
@ -595,17 +564,19 @@ class DriverService extends BaseService {
|
||||
const svc_registry = this.services.get('registry');
|
||||
const c_interfaces = svc_registry.get('interfaces');
|
||||
const c_types = svc_registry.get('types');
|
||||
|
||||
const svc_apiError = this.services.get('api-error');
|
||||
|
||||
// Note: 'interface' is a strict mode reserved word.
|
||||
const interface_ = c_interfaces.get(interface_name);
|
||||
if ( ! interface_ ) {
|
||||
throw APIError.create('interface_not_found', null, { interface_name });
|
||||
throw svc_apiError.create('interface_not_found', { interface_name });
|
||||
}
|
||||
|
||||
const processed_args = {};
|
||||
const method = interface_.methods[method_name];
|
||||
if ( ! method ) {
|
||||
throw APIError.create('method_not_found', null, { interface_name, method_name });
|
||||
throw svc_apiError.create('method_not_found', { interface_name, method_name });
|
||||
}
|
||||
|
||||
for ( const [arg_name, arg_descriptor] of Object.entries(method.parameters) ) {
|
||||
@ -616,7 +587,7 @@ class DriverService extends BaseService {
|
||||
// There's a particular way I want to do this that involves
|
||||
// a trait for extensible behaviour.
|
||||
if ( arg_value === undefined && arg_descriptor.required ) {
|
||||
throw APIError.create('missing_required_argument', null, {
|
||||
throw svc_apiError.create('missing_required_argument', {
|
||||
interface_name,
|
||||
method_name,
|
||||
arg_name,
|
||||
@ -629,7 +600,7 @@ class DriverService extends BaseService {
|
||||
processed_args[arg_name] = await arg_behaviour.consolidate(
|
||||
ctx, arg_value, { arg_descriptor, arg_name });
|
||||
} catch ( e ) {
|
||||
throw APIError.create('argument_consolidation_failed', null, {
|
||||
throw svc_apiError.create('argument_consolidation_failed', {
|
||||
interface_name,
|
||||
method_name,
|
||||
arg_name,
|
||||
|
@ -84,18 +84,12 @@ class DriverUsagePolicyService extends BaseService {
|
||||
actor,
|
||||
PermissionUtil.join('service', service_name, 'ii', trait_name),
|
||||
);
|
||||
console.log({
|
||||
perm: PermissionUtil.join('service', service_name, 'ii', trait_name),
|
||||
reading: require('util').inspect(reading, { depth: null }),
|
||||
});
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
console.log('OPTIONS', JSON.stringify(options, undefined, ' '));
|
||||
if ( options.length <= 0 ) {
|
||||
return undefined;
|
||||
}
|
||||
const option = await this.select_best_option_(options);
|
||||
const policies = await this.get_policies_for_option_(option);
|
||||
console.log('SLA', JSON.stringify(policies, undefined, ' '));
|
||||
|
||||
// NOT FINAL: For now we apply monthly usage logic
|
||||
// to the first holder of the permission. Later this
|
||||
|
@ -291,4 +291,7 @@ module.exports = {
|
||||
...ENTITY_STORAGE_INTERFACE,
|
||||
description: 'Read notifications on Puter.',
|
||||
},
|
||||
'crud-q': {
|
||||
...ENTITY_STORAGE_INTERFACE,
|
||||
},
|
||||
};
|
||||
|
@ -23,6 +23,13 @@ class Context {
|
||||
static USE_NAME_FALLBACK = {};
|
||||
static next_name_ = 0;
|
||||
static other_next_names_ = {};
|
||||
|
||||
// Context hooks should be registered via service (ContextService.js)
|
||||
static context_hooks_ = {
|
||||
pre_create: [],
|
||||
post_create: [],
|
||||
pre_arun: [],
|
||||
};
|
||||
|
||||
static contextAsyncLocalStorage = new AsyncLocalStorage();
|
||||
static __last_context_key = 0;
|
||||
@ -59,8 +66,8 @@ class Context {
|
||||
static describe () {
|
||||
return this.get().describe();
|
||||
}
|
||||
static arun (cb) {
|
||||
return this.get().arun(cb);
|
||||
static arun (...a) {
|
||||
return this.get().arun(...a);
|
||||
}
|
||||
static sub (values, opt_name) {
|
||||
return this.get().sub(values, opt_name);
|
||||
@ -72,6 +79,14 @@ class Context {
|
||||
this.values_[k] = v;
|
||||
}
|
||||
sub (values, opt_name) {
|
||||
if ( typeof values === 'string' ) {
|
||||
opt_name = values;
|
||||
values = {};
|
||||
}
|
||||
const name = opt_name ?? this.name ?? this.get('name');
|
||||
for ( const hook of this.constructor.context_hooks_.pre_create ) {
|
||||
hook({ values, name });
|
||||
}
|
||||
return new Context(values, this, opt_name);
|
||||
}
|
||||
get values () {
|
||||
@ -99,6 +114,7 @@ class Context {
|
||||
|
||||
opt_parent = opt_parent || Context.root;
|
||||
|
||||
this.trace_name = opt_name ?? undefined;
|
||||
this.name = (() => {
|
||||
if ( opt_name === this.constructor.USE_NAME_FALLBACK ) {
|
||||
opt_name = 'F';
|
||||
@ -129,7 +145,34 @@ class Context {
|
||||
|
||||
this.values_ = values;
|
||||
}
|
||||
async arun (cb) {
|
||||
async arun (...args) {
|
||||
let cb = args.shift();
|
||||
|
||||
let hints = {};
|
||||
if ( typeof cb === 'object' ) {
|
||||
hints = cb;
|
||||
cb = args.shift();
|
||||
}
|
||||
|
||||
if ( typeof cb === 'string' ) {
|
||||
const sub_context = this.sub(cb);
|
||||
return await sub_context.arun({ trace: true }, ...args);
|
||||
}
|
||||
|
||||
const replace_callback = new_cb => {
|
||||
cb = new_cb;
|
||||
}
|
||||
|
||||
for ( const hook of this.constructor.context_hooks_.pre_arun ) {
|
||||
hook({
|
||||
hints,
|
||||
name: this.name ?? this.get('name'),
|
||||
trace_name: this.trace_name,
|
||||
replace_callback,
|
||||
callback: cb,
|
||||
});
|
||||
}
|
||||
|
||||
const als = this.constructor.contextAsyncLocalStorage;
|
||||
return await als.run(new Map(), async () => {
|
||||
als.getStore().set('context', this);
|
||||
|
@ -169,7 +169,7 @@
|
||||
<!---------------------------------------->
|
||||
<!-- Edit App -->
|
||||
<!---------------------------------------->
|
||||
<section id="edit-app">
|
||||
<section id="edit-app" style="margin-bottom: 100px;">
|
||||
</section>
|
||||
|
||||
<!---------------------------------------->
|
||||
|
@ -518,9 +518,11 @@ function generate_edit_app_section(app) {
|
||||
</div>
|
||||
|
||||
<div class="section-tab" data-tab="info">
|
||||
<form style="clear:both;">
|
||||
<form style="clear:both; padding-bottom: 50px;">
|
||||
<div class="error" id="edit-app-error"></div>
|
||||
<div class="success" id="edit-app-success">App has been successfully updated.<span class="close-success-msg">×</span></div>
|
||||
<div class="success" id="edit-app-success">App has been successfully updated.<span class="close-success-msg">×</span>
|
||||
<p style="margin-bottom:0;"><span class="open-app button button-action" data-uid="${html_encode(app.uid)}" data-app-name="${html_encode(app.name)}">Give it a try!</span></p>
|
||||
</div>
|
||||
<input type="hidden" id="edit-app-uid" value="${html_encode(app.uid)}">
|
||||
|
||||
<h3 style="font-size: 23px; border-bottom: 1px solid #EEE; margin-top: 40px;">Basic</h3>
|
||||
@ -612,9 +614,10 @@ function generate_edit_app_section(app) {
|
||||
<p><code>credentialless</code> attribute for the <code>iframe</code> tag.</p>
|
||||
</div>
|
||||
|
||||
<hr style="margin-top: 40px;">
|
||||
<button type="button" class="edit-app-save-btn button button-primary">Save</button>
|
||||
<button type="button" class="edit-app-reset-btn button button-secondary">Reset</button>
|
||||
<div style="box-shadow: 10px 10px 15px #8c8c8c; overflow: hidden; position: fixed; bottom: 0; background: white; padding: 10px; width: 100%; left: 0;">
|
||||
<button type="button" class="edit-app-save-btn button button-primary" style="margin-right: 40px;">Save</button>
|
||||
<button type="button" class="edit-app-reset-btn button button-secondary">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`
|
||||
|
@ -88,16 +88,8 @@ function UITaskbarItem(options){
|
||||
);
|
||||
|
||||
if(options.onClick === undefined || options.onClick(el_taskbar_item) === false){
|
||||
const clicked_window = $(`.window[data-app="${options.app}"]`)
|
||||
|
||||
// hide window, unless there's more than one in app group
|
||||
if (clicked_window.hasClass("window-active") && clicked_window.length < 2) {
|
||||
clicked_window.hideWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
// re-show each window in this app group
|
||||
clicked_window.showWindow();
|
||||
$(`.window[data-app="${options.app}"]`).showWindow();
|
||||
}
|
||||
})
|
||||
|
||||
@ -228,8 +220,7 @@ function UITaskbarItem(options){
|
||||
menu_items.push({
|
||||
html: i18n('show_all_windows'),
|
||||
onClick: function(){
|
||||
if(open_windows > 0)
|
||||
$(el_taskbar_item).trigger('click');
|
||||
$(`.window[data-app="${options.app}"]`).showWindow();
|
||||
}
|
||||
})
|
||||
// -------------------------------------------
|
||||
|
@ -3420,6 +3420,10 @@ $.fn.showWindow = async function(options) {
|
||||
});
|
||||
$(el_window).css('z-index', ++window.last_window_zindex);
|
||||
|
||||
$(el_window).attr({
|
||||
'data-is_minimized': true,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
$(this).focusWindow();
|
||||
}, 80);
|
||||
|
@ -18,407 +18,450 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Traslation notes:
|
||||
* Traslation notes:
|
||||
* - Change all "Email" to "Correo electrónico"
|
||||
* - puter_description the most acurated translation for "privacy-first personal cloud" I could think of is "servicio de nube personal enfocado en privacidad"
|
||||
* - plural_suffix: 's' has no direct translation to spanish. There are multiple plural suffix in spanish 'as' || "es" || "os || "s". Leave "s" as it is only been used on item: 'elemento' and will end up as 'elementos'
|
||||
*/
|
||||
*/
|
||||
|
||||
const es = {
|
||||
name: "Español",
|
||||
english_name: "Spanish",
|
||||
code: "es",
|
||||
dictionary: {
|
||||
about: "Acerca De",
|
||||
account: "Cuenta",
|
||||
account_password: "Verifica Contraseña De La Cuenta",
|
||||
access_granted_to: "Acceso Permitido A",
|
||||
add_existing_account: "Añadir una cuenta existente",
|
||||
all_fields_required: 'Todos los campos son obligatorios.',
|
||||
allow: 'Permitir',
|
||||
apply: "Aplicar",
|
||||
ascending: 'Ascendiente',
|
||||
associated_websites: "Sitios Web Asociados",
|
||||
auto_arrange: 'Organización Automática',
|
||||
background: "Fondo",
|
||||
browse: "Buscar",
|
||||
cancel: 'Cancelar',
|
||||
center: 'Centrar',
|
||||
change_desktop_background: 'Cambiar el fondo de pantalla…',
|
||||
change_email: "Cambiar Correo Electrónico",
|
||||
change_language: "Cambiar Idioma",
|
||||
change_password: "Cambiar Contraseña",
|
||||
change_ui_colors: "Cambiar colores de la interfaz",
|
||||
change_username: "Cambiar Nombre de Usuario",
|
||||
close: 'Cerrar',
|
||||
close_all_windows: "Cerrar todas las ventanas",
|
||||
close_all_windows_confirm: "¿Estás seguro de que quieres cerrar todas las ventanas?",
|
||||
close_all_windows_and_log_out: 'Cerrar ventanas y cerrar sesión',
|
||||
change_always_open_with: "¿Quieres abrir siempre este tipo de archivos con",
|
||||
color: 'Color',
|
||||
confirm: 'Confirmar',
|
||||
confirm_2fa_setup: 'He añadido el código a mi aplicación de autenticación',
|
||||
confirm_2fa_recovery: 'He guardado mis códigos de recuperación en un lugar seguro',
|
||||
confirm_account_for_free_referral_storage_c2a: 'Crea una cuenta y confirma tu correo electrónico para recibir 1 GB de almacenamiento gratuito. Tu amigo recibirá 1 GB de almacenamiento gratuito también.',
|
||||
confirm_code_generic_incorrect: "Código incorrecto.",
|
||||
confirm_code_generic_too_many_requests: "Too many requests. Please wait a few minutes.",
|
||||
confirm_code_generic_submit: "Enviar código",
|
||||
confirm_code_generic_try_again: "Intenta nuevamente",
|
||||
confirm_code_generic_title: "Enter Confirmation Code",
|
||||
confirm_code_2fa_instruction: "Ingresa los 6 dígitos de tu aplicación de autenticación.",
|
||||
confirm_code_2fa_submit_btn: "Enviar",
|
||||
confirm_code_2fa_title: "Ingrese el código de 2FA",
|
||||
confirm_delete_multiple_items: '¿Estás seguro de que quieres eliminar permanentemente estos elementos?',
|
||||
confirm_delete_single_item: '¿Quieres eliminar este elemento permanentemente?',
|
||||
confirm_open_apps_log_out: 'Tienes aplicaciones abiertas.¿Estás seguro de que quieres cerrar sesión?',
|
||||
confirm_new_password: "Confirma la Nueva Contraseña",
|
||||
confirm_delete_user: "¿Estás seguro que quieres borrar esta cuenta? Todos tus archivos e información serán borrados permanentemente. Esta acción no se puede deshacer.",
|
||||
confirm_delete_user_title: "¿Eliminar cuenta?",
|
||||
confirm_session_revoke: "¿Estás seguro de que quieres revocar esta sesión?",
|
||||
confirm_your_email_address: "Confirma tu dirección de correo electrónico",
|
||||
contact_us: "Contáctanos",
|
||||
contact_us_verification_required: "Debes tener un correo electrónico verificado para usar esto.",
|
||||
contain: 'Contiene',
|
||||
continue: "Continuar",
|
||||
copy: 'Copiar',
|
||||
copy_link: "Copiar Enlace",
|
||||
copying: "Copiando",
|
||||
copying_file: "Copiando %%",
|
||||
cover: 'Cubrir',
|
||||
create_account: "Crear una cuenta",
|
||||
create_free_account: "Crear una cuenta gratuita",
|
||||
create_shortcut: "Crear un acceso directo",
|
||||
credits: "Creditos",
|
||||
current_password: "Contraseña actual",
|
||||
cut: 'Cortar',
|
||||
clock: "Reloj",
|
||||
clock_visible_hide: 'Ocultar - Siempre oculto',
|
||||
clock_visible_show: 'Mostrar - Siempre visible',
|
||||
clock_visible_auto: 'Auto - Por defecto, visible solo en modo pantalla completa.',
|
||||
close_all: 'Cerrar todo',
|
||||
created: 'Creado',
|
||||
date_modified: 'Fecha de modificación',
|
||||
default: 'Por defecto',
|
||||
delete: 'Borrar',
|
||||
delete_account: "Borrar cuenta",
|
||||
delete_permanently: "Borrar permanentemente",
|
||||
deleting_file: "Eliminando %%",
|
||||
deploy_as_app: 'Desplegar como una aplicación',
|
||||
descending: 'Descendiente',
|
||||
desktop: 'Escritorio',
|
||||
desktop_background_fit: "Ajustar",
|
||||
developers: "Desarrolladores",
|
||||
dir_published_as_website: `%strong% ha sido publicado en:`,
|
||||
disable_2fa: 'Deshabilitar 2FA',
|
||||
disable_2fa_confirm: "¿Estás seguro que quieres deshabilitar 2FA?",
|
||||
disable_2fa_instructions: "Ingresa tu contraseña para deshabilitar 2FA.",
|
||||
disassociate_dir: "Desvincular directorio",
|
||||
documents: 'Documentos',
|
||||
dont_allow: 'No permitir',
|
||||
download: 'Descargar',
|
||||
download_file: 'Descargar archivo',
|
||||
downloading: "Descargando",
|
||||
email: "Correo electrónico",
|
||||
email_change_confirmation_sent: "Se ha enviado un mensaje de confirmación a tu nueva dirección de correo electrónico. Por favor, revisa tu bandeja de entrada y sigue las instrucciónes para completar el proceso.",
|
||||
email_invalid: 'El correo electrónico no es válido.',
|
||||
email_or_username: "Correo electrónico o Nombre de Usuario",
|
||||
email_required: 'El correo electrónico es obligatorio.',
|
||||
empty_trash: 'Vaciar la papelera',
|
||||
empty_trash_confirmation: `¿Estás seguro de que quieres borrar permanentemente todos los elementos de la Papelera?`,
|
||||
emptying_trash: 'Vaciando la papelera…',
|
||||
enable_2fa: 'Habilitar 2FA',
|
||||
end_hard: "Finalizar abruptamente",
|
||||
end_process_force_confirm: "¿Estás seguro de que quieres forzar la salida de este proceso?",
|
||||
end_soft: "Finalizar suavemente",
|
||||
enlarged_qr_code: "Código QR ampliado",
|
||||
enter_password_to_confirm_delete_user: "Ingresa tu contraseña para confirmar la eliminación de la cuenta",
|
||||
error_message_is_missing: "Falta el mensaje de error.",
|
||||
error_unknown_cause: "Un error desconocido a ocurrido.",
|
||||
error_uploading_files: "Error al subir archivos",
|
||||
favorites: "Favoritos",
|
||||
feedback: "Sugerencias",
|
||||
feedback_c2a: "Por favor, usa el formulario para enviarnos tus sugerencias, comentarios y reporte de errores.",
|
||||
feedback_sent_confirmation: "Gracias por ponerte en contacto con nosotros. Si tienes un correo electrónico vinculado a esta cuenta, nos pondremos en contacto contigo tan pronto como podamos.",
|
||||
fit: "Ajustar",
|
||||
folder: 'Carpeta',
|
||||
force_quit: 'Forzar cierre',
|
||||
forgot_pass_c2a: "¿Olvidaste tu contraseña?",
|
||||
from: "De",
|
||||
general: "General",
|
||||
get_a_copy_of_on_puter: `¡Consigue una copia de '%%' en Puter.com!`,
|
||||
get_copy_link: 'Copiar el enlace',
|
||||
hide_all_windows: "Ocultar todas las ventanas",
|
||||
home: 'Inicio',
|
||||
html_document: 'Documento HTML',
|
||||
hue: 'Hue',
|
||||
image: 'Imagen',
|
||||
incorrect_password: "Contraseña incorrecta",
|
||||
invite_link: "Enlace de invitación",
|
||||
item: 'elemento',
|
||||
items_in_trash_cannot_be_renamed: `Este elemento no se puede renombrar porque está en la papelera. Para cambiar el nombre de este archivo, primero extráelo fuera de la misma.`,
|
||||
jpeg_image: 'Imagen JPEG',
|
||||
keep_in_taskbar: 'Mantener en la barra de tareas',
|
||||
language: "Lenguage",
|
||||
license: "Licencia",
|
||||
lightness: 'Claridad',
|
||||
link_copied: "Enlace copiado",
|
||||
loading: 'Cargando',
|
||||
log_in: "Iniciar sesión",
|
||||
log_into_another_account_anyway: 'Iniciar sesión en otra cuenta de todos modos',
|
||||
log_out: 'Cerrar sesión',
|
||||
looks_good: "Se ve bien!",
|
||||
manage_sessions: "Administrar sesión",
|
||||
menubar_style: "Estilo de la barra de menú",
|
||||
menubar_style_desktop: "Escritorio",
|
||||
menubar_style_system: "Sistema",
|
||||
menubar_style_window: "Ventana",
|
||||
modified: 'Modified',
|
||||
move: 'Mover',
|
||||
moving_file: "Moviendo %%",
|
||||
my_websites: "Mis páginas web",
|
||||
name: 'Nombre',
|
||||
name_cannot_be_empty: 'El nombre no puede estar vacío.',
|
||||
name_cannot_contain_double_period: "El nombre no puede ser el carácter '..'.",
|
||||
name_cannot_contain_period: "El nombre no puede ser el carácter '.'.",
|
||||
name_cannot_contain_slash: "El nombre no puede contener el carácter '/'.",
|
||||
name_must_be_string: "El nombre debe ser una cadena de texto.",
|
||||
name_too_long: `El nombre no puede tener más de %% caracteres.`,
|
||||
new: 'Nuevo',
|
||||
new_email: 'Nuevo correo electrónico',
|
||||
new_folder: 'Nueva carpeta',
|
||||
new_password: "Nueva contraseña",
|
||||
new_username: "Nuevo nombre de usuario",
|
||||
no: 'No',
|
||||
no_dir_associated_with_site: 'No hay un directorio vinculado con esta dirección.',
|
||||
no_websites_published: "Aun no has publicado ningún sitio web. Haz click derecho en una carpeta para empezar",
|
||||
ok: 'OK',
|
||||
open: "Abrir",
|
||||
open_in_new_tab: "Abrir en una nueva pestaña",
|
||||
open_in_new_window: "Abrir en una nueva ventana",
|
||||
open_with: "Abrir con",
|
||||
original_name: 'Nombre original',
|
||||
original_path: 'Ruta original',
|
||||
oss_code_and_content: "Software y contenido de código abierto",
|
||||
password: "Contraseña",
|
||||
password_changed: "Contraseña cambiada.",
|
||||
password_recovery_rate_limit: "Haz alcanzado nuestra tasa de refresco; por favor espera unos minutos. Para evitar esto en el futuro, evita refrescar la página muchas veces.",
|
||||
password_recovery_token_invalid: "La contraseña de token de recuperación ya no es válida.",
|
||||
password_recovery_unknown_error: "Ocurrió un error desconocido. Por favor, inténtalo de nuevo más tarde.",
|
||||
password_required: 'La contraseña es obligatoria.',
|
||||
password_strength_error: "La contraseña debe tener almenos 8 caracteres de largo y contener almenos una letra mayúscula, una minúscula, un numero, y un caracter especial.",
|
||||
passwords_do_not_match: '`Nueva Contraseña` y `Confirmar Nueva Contraseña` no coinciden.',
|
||||
paste: 'Pegar',
|
||||
paste_into_folder: "Pegar en la Carpeta",
|
||||
path: 'Ruta',
|
||||
personalization: "Personalización",
|
||||
pick_name_for_website: "Escoge un nombre para tu página web:",
|
||||
picture: "Imagen",
|
||||
pictures: 'Imagenes',
|
||||
plural_suffix: 's',
|
||||
powered_by_puter_js: `Creado por {{link=docs}}Puter.js{{/link}}`,
|
||||
preparing: "Preparando...",
|
||||
preparing_for_upload: "Preparando para la subida...",
|
||||
print: 'Imprimir',
|
||||
privacy: "Privacidad",
|
||||
proceed_to_login: 'Procede a iniciar sesión',
|
||||
proceed_with_account_deletion: "Procede con la eliminación de la cuenta",
|
||||
process_status_initializing: "Inicializando",
|
||||
process_status_running: "El ejecución",
|
||||
process_type_app: 'Aplicación',
|
||||
process_type_init: 'Inicialización',
|
||||
process_type_ui: 'Interfaz de usuario',
|
||||
properties: "Propiedades",
|
||||
public: 'Publico',
|
||||
publish: "Publicar",
|
||||
publish_as_website: 'Publicar como página web',
|
||||
puter_description: `Puter es un servicio de nube personal enfocado en privacidad que mantiene tus archivos, aplicaciónes, y juegos en un solo lugar, accesible desde cualquier lugar en cualquier momento.`,
|
||||
reading_file: "Leyendo %strong%",
|
||||
recent: "Reciente",
|
||||
recommended: "Recomendado",
|
||||
recover_password: "Recuperar Contraseña",
|
||||
refer_friends_c2a: "Consigue 1 GB por cada amigo que cree y confirme una cuenta en Puter ¡Tu amigo recibirá 1GB también!",
|
||||
refer_friends_social_media_c2a: `¡Consigue 1 GB de almacenamiento gratuito en Puter.com!`,
|
||||
refresh: 'Refrescar',
|
||||
release_address_confirmation: `¿Estás seguro de que quieres liberar esta dirección?`,
|
||||
remove_from_taskbar:'Eliminar de la barra de tareas',
|
||||
rename: 'Renombrar',
|
||||
repeat: 'Repetir',
|
||||
replace: 'Remplazar',
|
||||
replace_all: 'Replace All',
|
||||
resend_confirmation_code: "Reenviar Código de Confirmación",
|
||||
reset_colors: "Restablecer colores",
|
||||
restart_puter_confirm: "¿Estás seguro que deseas reiniciar Puter?",
|
||||
restore: "Restaurar",
|
||||
save: 'Guardar',
|
||||
saturation: 'Saturación',
|
||||
save_account: 'Guardar cuenta',
|
||||
save_account_to_get_copy_link: "Por favor, crea una cuenta para continuar.",
|
||||
save_account_to_publish: 'Por favor, crea una cuenta para continuar.',
|
||||
save_session: 'Guardar sesión',
|
||||
save_session_c2a: 'Crea una cuenta para guardar tu sesión actual y evitar así perder tu trabajo.',
|
||||
scan_qr_c2a: 'Escanee el código a continuación para inicia sesión desde otros dispositivos',
|
||||
scan_qr_2fa: 'Escanee el codigo QR con su aplicación de autenticación',
|
||||
scan_qr_generic: 'Scan this QR code using your phone or another device',
|
||||
search: 'Buscar',
|
||||
seconds: 'segundos',
|
||||
security: "Seguridad",
|
||||
select: "Seleccionar",
|
||||
selected: 'seleccionado',
|
||||
select_color: 'Seleccionar color…',
|
||||
sessions: "Sesión",
|
||||
send: "Enviar",
|
||||
send_password_recovery_email: "Enviar la contraseña al correo de recuperación",
|
||||
session_saved: "Gracias por crear una cuenta. La sesión ha sido guardada.",
|
||||
set_new_password: "Establecer una nueva contraseña",
|
||||
settings: "Opciones",
|
||||
share: "Compartir",
|
||||
share_to: "Compartir a",
|
||||
share_with: "Compartir con:",
|
||||
shortcut_to: "Acceso directo a",
|
||||
show_all_windows: "Mostrar todas las ventanas",
|
||||
show_hidden: 'Mostrar ocultos',
|
||||
sign_in_with_puter: "Inicia sesión con Puter",
|
||||
sign_up: "Registrarse",
|
||||
signing_in: "Registrándose…",
|
||||
size: 'Tamaño',
|
||||
skip: 'Saltar',
|
||||
something_went_wrong: "Algo salió mal.",
|
||||
sort_by: 'Ordenar Por',
|
||||
start: 'Inicio',
|
||||
status: "Estado",
|
||||
storage_usage: "Uso del almacenamiento",
|
||||
storage_puter_used: 'Usado por Puter',
|
||||
taking_longer_than_usual: 'Tardando un poco más de lo habitual. Por favor, espere...',
|
||||
task_manager: "Administrador de tareas",
|
||||
taskmgr_header_name: "Nombre",
|
||||
taskmgr_header_status: "Estado",
|
||||
taskmgr_header_type: "Tipo",
|
||||
terms: "Terminos",
|
||||
text_document: 'Documento de Texto',
|
||||
tos_fineprint: `Al hacer clic en 'Crear una cuenta gratuita' aceptas los {{link=terms}}términos del servicio{{/link}} y {{link=privacy}}la política de privacidad{{/link}} de Puter.`,
|
||||
transparency: "Transparencia",
|
||||
trash: 'Papelera',
|
||||
two_factor: 'Autenticación de dos factores',
|
||||
two_factor_disabled: '2FA Deshabilitadp',
|
||||
two_factor_enabled: '2FA Habilitado',
|
||||
type: 'Tipo',
|
||||
type_confirm_to_delete_account: "Ingrese 'Confirmar' para borrar esta cuenta.",
|
||||
ui_colors: "Colores de interfaz",
|
||||
ui_manage_sessions: "Administrador de sesión",
|
||||
ui_revoke: "Revocar",
|
||||
undo: 'Deshacer',
|
||||
unlimited: 'Ilimitado',
|
||||
unzip: "Descomprimir",
|
||||
upload: 'Subir',
|
||||
upload_here: 'Subir aquí',
|
||||
usage: 'Uso',
|
||||
username: "Nombre de usuario",
|
||||
username_changed: 'Nombre de usuario actualizado correctamente.',
|
||||
username_required: 'El nombre de usuario es obligatorio.',
|
||||
versions: "Versiones",
|
||||
videos: 'Videos',
|
||||
visibility: 'Visibilidad',
|
||||
yes: 'Si',
|
||||
yes_release_it: 'Sí, libéralo',
|
||||
you_have_been_referred_to_puter_by_a_friend: "¡Has sido invitado a Puter por un amigo!",
|
||||
zip: "Zip",
|
||||
zipping_file: "Compriminendo %strong%",
|
||||
name: 'Español',
|
||||
english_name: 'Spanish',
|
||||
code: 'es',
|
||||
dictionary: {
|
||||
about: 'Acerca De',
|
||||
account: 'Cuenta',
|
||||
account_password: 'Verifica Contraseña De La Cuenta',
|
||||
access_granted_to: 'Acceso Permitido A',
|
||||
add_existing_account: 'Añadir una cuenta existente',
|
||||
all_fields_required: 'Todos los campos son obligatorios.',
|
||||
allow: 'Permitir',
|
||||
apply: 'Aplicar',
|
||||
ascending: 'Ascendiente',
|
||||
associated_websites: 'Sitios Web Asociados',
|
||||
auto_arrange: 'Organización Automática',
|
||||
background: 'Fondo',
|
||||
browse: 'Buscar',
|
||||
cancel: 'Cancelar',
|
||||
center: 'Centrar',
|
||||
change_desktop_background: 'Cambiar el fondo de pantalla…',
|
||||
change_email: 'Cambiar Correo Electrónico',
|
||||
change_language: 'Cambiar Idioma',
|
||||
change_password: 'Cambiar Contraseña',
|
||||
change_ui_colors: 'Cambiar colores de la interfaz',
|
||||
change_username: 'Cambiar Nombre de Usuario',
|
||||
close: 'Cerrar',
|
||||
close_all_windows: 'Cerrar todas las ventanas',
|
||||
close_all_windows_confirm:
|
||||
'¿Estás seguro de que quieres cerrar todas las ventanas?',
|
||||
close_all_windows_and_log_out: 'Cerrar ventanas y cerrar sesión',
|
||||
change_always_open_with: '¿Quieres abrir siempre este tipo de archivos con',
|
||||
color: 'Color',
|
||||
confirm: 'Confirmar',
|
||||
confirm_2fa_setup: 'He añadido el código a mi aplicación de autenticación',
|
||||
confirm_2fa_recovery:
|
||||
'He guardado mis códigos de recuperación en un lugar seguro',
|
||||
confirm_account_for_free_referral_storage_c2a:
|
||||
'Crea una cuenta y confirma tu correo electrónico para recibir 1 GB de almacenamiento gratuito. Tu amigo recibirá 1 GB de almacenamiento gratuito también.',
|
||||
confirm_code_generic_incorrect: 'Código incorrecto.',
|
||||
confirm_code_generic_too_many_requests:
|
||||
'Too many requests. Please wait a few minutes.',
|
||||
confirm_code_generic_submit: 'Enviar código',
|
||||
confirm_code_generic_try_again: 'Intenta nuevamente',
|
||||
confirm_code_generic_title: 'Enter Confirmation Code',
|
||||
confirm_code_2fa_instruction:
|
||||
'Ingresa los 6 dígitos de tu aplicación de autenticación.',
|
||||
confirm_code_2fa_submit_btn: 'Enviar',
|
||||
confirm_code_2fa_title: 'Ingrese el código de 2FA',
|
||||
confirm_delete_multiple_items:
|
||||
'¿Estás seguro de que quieres eliminar permanentemente estos elementos?',
|
||||
confirm_delete_single_item:
|
||||
'¿Quieres eliminar este elemento permanentemente?',
|
||||
confirm_open_apps_log_out:
|
||||
'Tienes aplicaciones abiertas.¿Estás seguro de que quieres cerrar sesión?',
|
||||
confirm_new_password: 'Confirma la Nueva Contraseña',
|
||||
confirm_delete_user:
|
||||
'¿Estás seguro que quieres borrar esta cuenta? Todos tus archivos e información serán borrados permanentemente. Esta acción no se puede deshacer.',
|
||||
confirm_delete_user_title: '¿Eliminar cuenta?',
|
||||
confirm_session_revoke: '¿Estás seguro de que quieres revocar esta sesión?',
|
||||
confirm_your_email_address: 'Confirma tu dirección de correo electrónico',
|
||||
contact_us: 'Contáctanos',
|
||||
contact_us_verification_required:
|
||||
'Debes tener un correo electrónico verificado para usar esto.',
|
||||
contain: 'Contiene',
|
||||
continue: 'Continuar',
|
||||
copy: 'Copiar',
|
||||
copy_link: 'Copiar Enlace',
|
||||
copying: 'Copiando',
|
||||
copying_file: 'Copiando %%',
|
||||
cover: 'Cubrir',
|
||||
create_account: 'Crear una cuenta',
|
||||
create_free_account: 'Crear una cuenta gratuita',
|
||||
create_shortcut: 'Crear un acceso directo',
|
||||
credits: 'Creditos',
|
||||
current_password: 'Contraseña actual',
|
||||
cut: 'Cortar',
|
||||
clock: 'Reloj',
|
||||
clock_visible_hide: 'Ocultar - Siempre oculto',
|
||||
clock_visible_show: 'Mostrar - Siempre visible',
|
||||
clock_visible_auto:
|
||||
'Auto - Por defecto, visible solo en modo pantalla completa.',
|
||||
close_all: 'Cerrar todo',
|
||||
created: 'Creado',
|
||||
date_modified: 'Fecha de modificación',
|
||||
default: 'Por defecto',
|
||||
delete: 'Borrar',
|
||||
delete_account: 'Borrar cuenta',
|
||||
delete_permanently: 'Borrar permanentemente',
|
||||
deleting_file: 'Eliminando %%',
|
||||
deploy_as_app: 'Desplegar como una aplicación',
|
||||
descending: 'Descendiente',
|
||||
desktop: 'Escritorio',
|
||||
desktop_background_fit: 'Ajustar',
|
||||
developers: 'Desarrolladores',
|
||||
dir_published_as_website: `%strong% ha sido publicado en:`,
|
||||
disable_2fa: 'Deshabilitar 2FA',
|
||||
disable_2fa_confirm: '¿Estás seguro que quieres deshabilitar 2FA?',
|
||||
disable_2fa_instructions: 'Ingresa tu contraseña para deshabilitar 2FA.',
|
||||
disassociate_dir: 'Desvincular directorio',
|
||||
documents: 'Documentos',
|
||||
dont_allow: 'No permitir',
|
||||
download: 'Descargar',
|
||||
download_file: 'Descargar archivo',
|
||||
downloading: 'Descargando',
|
||||
email: 'Correo electrónico',
|
||||
email_change_confirmation_sent:
|
||||
'Se ha enviado un mensaje de confirmación a tu nueva dirección de correo electrónico. Por favor, revisa tu bandeja de entrada y sigue las instrucciónes para completar el proceso.',
|
||||
email_invalid: 'El correo electrónico no es válido.',
|
||||
email_or_username: 'Correo electrónico o Nombre de Usuario',
|
||||
email_required: 'El correo electrónico es obligatorio.',
|
||||
empty_trash: 'Vaciar la papelera',
|
||||
empty_trash_confirmation: `¿Estás seguro de que quieres borrar permanentemente todos los elementos de la Papelera?`,
|
||||
emptying_trash: 'Vaciando la papelera…',
|
||||
enable_2fa: 'Habilitar 2FA',
|
||||
end_hard: 'Finalizar abruptamente',
|
||||
end_process_force_confirm:
|
||||
'¿Estás seguro de que quieres forzar la salida de este proceso?',
|
||||
end_soft: 'Finalizar suavemente',
|
||||
enlarged_qr_code: 'Código QR ampliado',
|
||||
enter_password_to_confirm_delete_user:
|
||||
'Ingresa tu contraseña para confirmar la eliminación de la cuenta',
|
||||
error_message_is_missing: 'Falta el mensaje de error.',
|
||||
error_unknown_cause: 'Un error desconocido a ocurrido.',
|
||||
error_uploading_files: 'Error al subir archivos',
|
||||
favorites: 'Favoritos',
|
||||
feedback: 'Sugerencias',
|
||||
feedback_c2a:
|
||||
'Por favor, usa el formulario para enviarnos tus sugerencias, comentarios y reporte de errores.',
|
||||
feedback_sent_confirmation:
|
||||
'Gracias por ponerte en contacto con nosotros. Si tienes un correo electrónico vinculado a esta cuenta, nos pondremos en contacto contigo tan pronto como podamos.',
|
||||
fit: 'Ajustar',
|
||||
folder: 'Carpeta',
|
||||
force_quit: 'Forzar cierre',
|
||||
forgot_pass_c2a: '¿Olvidaste tu contraseña?',
|
||||
from: 'De',
|
||||
general: 'General',
|
||||
get_a_copy_of_on_puter: `¡Consigue una copia de '%%' en Puter.com!`,
|
||||
get_copy_link: 'Copiar el enlace',
|
||||
hide_all_windows: 'Ocultar todas las ventanas',
|
||||
home: 'Inicio',
|
||||
html_document: 'Documento HTML',
|
||||
hue: 'Hue',
|
||||
image: 'Imagen',
|
||||
incorrect_password: 'Contraseña incorrecta',
|
||||
invite_link: 'Enlace de invitación',
|
||||
item: 'elemento',
|
||||
items_in_trash_cannot_be_renamed: `Este elemento no se puede renombrar porque está en la papelera. Para cambiar el nombre de este archivo, primero extráelo fuera de la misma.`,
|
||||
jpeg_image: 'Imagen JPEG',
|
||||
keep_in_taskbar: 'Mantener en la barra de tareas',
|
||||
language: 'Lenguage',
|
||||
license: 'Licencia',
|
||||
lightness: 'Claridad',
|
||||
link_copied: 'Enlace copiado',
|
||||
loading: 'Cargando',
|
||||
log_in: 'Iniciar sesión',
|
||||
log_into_another_account_anyway:
|
||||
'Iniciar sesión en otra cuenta de todos modos',
|
||||
log_out: 'Cerrar sesión',
|
||||
looks_good: 'Se ve bien!',
|
||||
manage_sessions: 'Administrar sesión',
|
||||
menubar_style: 'Estilo de la barra de menú',
|
||||
menubar_style_desktop: 'Escritorio',
|
||||
menubar_style_system: 'Sistema',
|
||||
menubar_style_window: 'Ventana',
|
||||
modified: 'Modified',
|
||||
move: 'Mover',
|
||||
moving_file: 'Moviendo %%',
|
||||
my_websites: 'Mis páginas web',
|
||||
name: 'Nombre',
|
||||
name_cannot_be_empty: 'El nombre no puede estar vacío.',
|
||||
name_cannot_contain_double_period:
|
||||
"El nombre no puede ser el carácter '..'.",
|
||||
name_cannot_contain_period: "El nombre no puede ser el carácter '.'.",
|
||||
name_cannot_contain_slash: "El nombre no puede contener el carácter '/'.",
|
||||
name_must_be_string: 'El nombre debe ser una cadena de texto.',
|
||||
name_too_long: `El nombre no puede tener más de %% caracteres.`,
|
||||
new: 'Nuevo',
|
||||
new_email: 'Nuevo correo electrónico',
|
||||
new_folder: 'Nueva carpeta',
|
||||
new_password: 'Nueva contraseña',
|
||||
new_username: 'Nuevo nombre de usuario',
|
||||
no: 'No',
|
||||
no_dir_associated_with_site:
|
||||
'No hay un directorio vinculado con esta dirección.',
|
||||
no_websites_published:
|
||||
'Aun no has publicado ningún sitio web. Haz click derecho en una carpeta para empezar',
|
||||
ok: 'OK',
|
||||
open: 'Abrir',
|
||||
open_in_new_tab: 'Abrir en una nueva pestaña',
|
||||
open_in_new_window: 'Abrir en una nueva ventana',
|
||||
open_with: 'Abrir con',
|
||||
original_name: 'Nombre original',
|
||||
original_path: 'Ruta original',
|
||||
oss_code_and_content: 'Software y contenido de código abierto',
|
||||
password: 'Contraseña',
|
||||
password_changed: 'Contraseña cambiada.',
|
||||
password_recovery_rate_limit:
|
||||
'Haz alcanzado nuestra tasa de refresco; por favor espera unos minutos. Para evitar esto en el futuro, evita refrescar la página muchas veces.',
|
||||
password_recovery_token_invalid:
|
||||
'La contraseña de token de recuperación ya no es válida.',
|
||||
password_recovery_unknown_error:
|
||||
'Ocurrió un error desconocido. Por favor, inténtalo de nuevo más tarde.',
|
||||
password_required: 'La contraseña es obligatoria.',
|
||||
password_strength_error:
|
||||
'La contraseña debe tener almenos 8 caracteres de largo y contener almenos una letra mayúscula, una minúscula, un numero, y un caracter especial.',
|
||||
passwords_do_not_match:
|
||||
'`Nueva Contraseña` y `Confirmar Nueva Contraseña` no coinciden.',
|
||||
paste: 'Pegar',
|
||||
paste_into_folder: 'Pegar en la Carpeta',
|
||||
path: 'Ruta',
|
||||
personalization: 'Personalización',
|
||||
pick_name_for_website: 'Escoge un nombre para tu página web:',
|
||||
picture: 'Imagen',
|
||||
pictures: 'Imagenes',
|
||||
plural_suffix: 's',
|
||||
powered_by_puter_js: `Creado por {{link=docs}}Puter.js{{/link}}`,
|
||||
preparing: 'Preparando...',
|
||||
preparing_for_upload: 'Preparando para la subida...',
|
||||
print: 'Imprimir',
|
||||
privacy: 'Privacidad',
|
||||
proceed_to_login: 'Procede a iniciar sesión',
|
||||
proceed_with_account_deletion: 'Procede con la eliminación de la cuenta',
|
||||
process_status_initializing: 'Inicializando',
|
||||
process_status_running: 'El ejecución',
|
||||
process_type_app: 'Aplicación',
|
||||
process_type_init: 'Inicialización',
|
||||
process_type_ui: 'Interfaz de usuario',
|
||||
properties: 'Propiedades',
|
||||
public: 'Publico',
|
||||
publish: 'Publicar',
|
||||
publish_as_website: 'Publicar como página web',
|
||||
puter_description: `Puter es un servicio de nube personal enfocado en privacidad que mantiene tus archivos, aplicaciónes, y juegos en un solo lugar, accesible desde cualquier lugar en cualquier momento.`,
|
||||
reading_file: 'Leyendo %strong%',
|
||||
recent: 'Reciente',
|
||||
recommended: 'Recomendado',
|
||||
recover_password: 'Recuperar Contraseña',
|
||||
refer_friends_c2a:
|
||||
'Consigue 1 GB por cada amigo que cree y confirme una cuenta en Puter ¡Tu amigo recibirá 1GB también!',
|
||||
refer_friends_social_media_c2a: `¡Consigue 1 GB de almacenamiento gratuito en Puter.com!`,
|
||||
refresh: 'Refrescar',
|
||||
release_address_confirmation: `¿Estás seguro de que quieres liberar esta dirección?`,
|
||||
remove_from_taskbar: 'Eliminar de la barra de tareas',
|
||||
rename: 'Renombrar',
|
||||
repeat: 'Repetir',
|
||||
replace: 'Remplazar',
|
||||
replace_all: 'Replace All',
|
||||
resend_confirmation_code: 'Reenviar Código de Confirmación',
|
||||
reset_colors: 'Restablecer colores',
|
||||
restart_puter_confirm: '¿Estás seguro que deseas reiniciar Puter?',
|
||||
restore: 'Restaurar',
|
||||
save: 'Guardar',
|
||||
saturation: 'Saturación',
|
||||
save_account: 'Guardar cuenta',
|
||||
save_account_to_get_copy_link: 'Por favor, crea una cuenta para continuar.',
|
||||
save_account_to_publish: 'Por favor, crea una cuenta para continuar.',
|
||||
save_session: 'Guardar sesión',
|
||||
save_session_c2a:
|
||||
'Crea una cuenta para guardar tu sesión actual y evitar así perder tu trabajo.',
|
||||
scan_qr_c2a:
|
||||
'Escanee el código a continuación para inicia sesión desde otros dispositivos',
|
||||
scan_qr_2fa: 'Escanee el codigo QR con su aplicación de autenticación',
|
||||
scan_qr_generic: 'Scan this QR code using your phone or another device',
|
||||
search: 'Buscar',
|
||||
seconds: 'segundos',
|
||||
security: 'Seguridad',
|
||||
select: 'Seleccionar',
|
||||
selected: 'seleccionado',
|
||||
select_color: 'Seleccionar color…',
|
||||
sessions: 'Sesión',
|
||||
send: 'Enviar',
|
||||
send_password_recovery_email:
|
||||
'Enviar la contraseña al correo de recuperación',
|
||||
session_saved: 'Gracias por crear una cuenta. La sesión ha sido guardada.',
|
||||
set_new_password: 'Establecer una nueva contraseña',
|
||||
settings: 'Opciones',
|
||||
share: 'Compartir',
|
||||
share_to: 'Compartir a',
|
||||
share_with: 'Compartir con:',
|
||||
shortcut_to: 'Acceso directo a',
|
||||
show_all_windows: 'Mostrar todas las ventanas',
|
||||
show_hidden: 'Mostrar ocultos',
|
||||
sign_in_with_puter: 'Inicia sesión con Puter',
|
||||
sign_up: 'Registrarse',
|
||||
signing_in: 'Registrándose…',
|
||||
size: 'Tamaño',
|
||||
skip: 'Saltar',
|
||||
something_went_wrong: 'Algo salió mal.',
|
||||
sort_by: 'Ordenar Por',
|
||||
start: 'Inicio',
|
||||
status: 'Estado',
|
||||
storage_usage: 'Uso del almacenamiento',
|
||||
storage_puter_used: 'Usado por Puter',
|
||||
taking_longer_than_usual:
|
||||
'Tardando un poco más de lo habitual. Por favor, espere...',
|
||||
task_manager: 'Administrador de tareas',
|
||||
taskmgr_header_name: 'Nombre',
|
||||
taskmgr_header_status: 'Estado',
|
||||
taskmgr_header_type: 'Tipo',
|
||||
terms: 'Terminos',
|
||||
text_document: 'Documento de Texto',
|
||||
tos_fineprint: `Al hacer clic en 'Crear una cuenta gratuita' aceptas los {{link=terms}}términos del servicio{{/link}} y {{link=privacy}}la política de privacidad{{/link}} de Puter.`,
|
||||
transparency: 'Transparencia',
|
||||
trash: 'Papelera',
|
||||
two_factor: 'Autenticación de dos factores',
|
||||
two_factor_disabled: '2FA Deshabilitadp',
|
||||
two_factor_enabled: '2FA Habilitado',
|
||||
type: 'Tipo',
|
||||
type_confirm_to_delete_account:
|
||||
"Ingrese 'Confirmar' para borrar esta cuenta.",
|
||||
ui_colors: 'Colores de interfaz',
|
||||
ui_manage_sessions: 'Administrador de sesión',
|
||||
ui_revoke: 'Revocar',
|
||||
undo: 'Deshacer',
|
||||
unlimited: 'Ilimitado',
|
||||
unzip: 'Descomprimir',
|
||||
upload: 'Subir',
|
||||
upload_here: 'Subir aquí',
|
||||
usage: 'Uso',
|
||||
username: 'Nombre de usuario',
|
||||
username_changed: 'Nombre de usuario actualizado correctamente.',
|
||||
username_required: 'El nombre de usuario es obligatorio.',
|
||||
versions: 'Versiones',
|
||||
videos: 'Videos',
|
||||
visibility: 'Visibilidad',
|
||||
yes: 'Si',
|
||||
yes_release_it: 'Sí, libéralo',
|
||||
you_have_been_referred_to_puter_by_a_friend:
|
||||
'¡Has sido invitado a Puter por un amigo!',
|
||||
zip: 'Zip',
|
||||
zipping_file: 'Compriminendo %strong%',
|
||||
|
||||
// === 2FA Setup ===
|
||||
setup2fa_1_step_heading: 'Abre tu aplicación de autenticación',
|
||||
setup2fa_1_instructions: `
|
||||
// === 2FA Setup ===
|
||||
setup2fa_1_step_heading: 'Abre tu aplicación de autenticación',
|
||||
setup2fa_1_instructions: `
|
||||
Puedes usar cualquier aplicación de autenticación que soporte el protocolo de Time-based One-time (TOTP).
|
||||
Hay muchos para elegir, pero si no estas seguro
|
||||
<a target="_blank" href="https://authy.com/download">Authy</a>
|
||||
es una opción segura para Android y iOS.
|
||||
`,
|
||||
setup2fa_2_step_heading: 'Escanea el código QR',
|
||||
setup2fa_3_step_heading: 'Ingresa el código de 6 dígitos',
|
||||
setup2fa_4_step_heading: 'Copiar tus códigos de recuperación',
|
||||
setup2fa_4_instructions: `
|
||||
setup2fa_2_step_heading: 'Escanea el código QR',
|
||||
setup2fa_3_step_heading: 'Ingresa el código de 6 dígitos',
|
||||
setup2fa_4_step_heading: 'Copiar tus códigos de recuperación',
|
||||
setup2fa_4_instructions: `
|
||||
Estos códigos de recuperación son la única forma de acceder a tu cuenta, si pierdes tu teléfono o no puedes usar la aplicación de autenticación.
|
||||
Asegurate de guardarlos en un lugar seguro.
|
||||
`,
|
||||
setup2fa_5_step_heading: 'Confirmar la configuración de 2FA',
|
||||
setup2fa_5_confirmation_1: 'He guardado mis códigos de recuperación en un lugar seguro',
|
||||
setup2fa_5_confirmation_2: 'Estoy listo para habilitar 2FA',
|
||||
setup2fa_5_button: 'Habilitar 2FA',
|
||||
setup2fa_5_step_heading: 'Confirmar la configuración de 2FA',
|
||||
setup2fa_5_confirmation_1:
|
||||
'He guardado mis códigos de recuperación en un lugar seguro',
|
||||
setup2fa_5_confirmation_2: 'Estoy listo para habilitar 2FA',
|
||||
setup2fa_5_button: 'Habilitar 2FA',
|
||||
|
||||
// === 2FA Login ===
|
||||
login2fa_otp_title: 'Ingresar el código 2FA',
|
||||
login2fa_otp_instructions: 'Ingresa tu código de 6 dígitos de tu aplicación de autenticación.',
|
||||
login2fa_recovery_title: 'Ingresa tu código de recuperación',
|
||||
login2fa_recovery_instructions: 'Ingresa uno de tus códigos de recuperación para acceder a tu cuenta.',
|
||||
login2fa_use_recovery_code: 'Usar un código de recuperación',
|
||||
login2fa_recovery_back: 'Atras',
|
||||
login2fa_recovery_placeholder: 'XXXXXXXX',
|
||||
// === 2FA Login ===
|
||||
login2fa_otp_title: 'Ingresar el código 2FA',
|
||||
login2fa_otp_instructions:
|
||||
'Ingresa tu código de 6 dígitos de tu aplicación de autenticación.',
|
||||
login2fa_recovery_title: 'Ingresa tu código de recuperación',
|
||||
login2fa_recovery_instructions:
|
||||
'Ingresa uno de tus códigos de recuperación para acceder a tu cuenta.',
|
||||
login2fa_use_recovery_code: 'Usar un código de recuperación',
|
||||
login2fa_recovery_back: 'Atras',
|
||||
login2fa_recovery_placeholder: 'XXXXXXXX',
|
||||
|
||||
"change": "cambiar", // In English: "Change"
|
||||
"clock_visibility": "visibilidadReloj", // In English: "Clock Visibility"
|
||||
"reading": "lectura %strong%", // In English: "Reading %strong%"
|
||||
"writing": "escribiendo %strong%", // In English: "Writing %strong%"
|
||||
"unzipping": "descomprimiendo %strong%", // In English: "Unzipping %strong%"
|
||||
"sequencing": "secuenciación %strong%", // In English: "Sequencing %strong%"
|
||||
"zipping": "comprimiendo %strong%", // In English: "Zipping %strong%"
|
||||
"Editor": "Editor", // In English: "Editor"
|
||||
"Viewer": "Espectador", // In English: "Viewer"
|
||||
"People with access": "Personas con acceso", // In English: "People with access"
|
||||
"Share With…": "Compartir con…", // In English: "Share With…"
|
||||
"Owner": "Propietario", // In English: "Owner"
|
||||
"You can't share with yourself.": "No puedes compartir contigo mismo.", // In English: "You can't share with yourself."
|
||||
"This user already has access to this item": "Este usuario ya tiene acceso a este elemento.", // In English: "This user already has access to this item"
|
||||
|
||||
// ----------------------------------------
|
||||
// Missing translations:
|
||||
// ----------------------------------------
|
||||
"billing.change_payment_method": undefined, // In English: "Change"
|
||||
"billing.cancel": undefined, // In English: "Cancel"
|
||||
"billing.download_invoice": undefined, // In English: "Download"
|
||||
"billing.payment_method": undefined, // In English: "Payment Method"
|
||||
"billing.payment_method_updated": undefined, // In English: "Payment method updated!"
|
||||
"billing.confirm_payment_method": undefined, // In English: "Confirm Payment Method"
|
||||
"billing.payment_history": undefined, // In English: "Payment History"
|
||||
"billing.refunded": undefined, // In English: "Refunded"
|
||||
"billing.paid": undefined, // In English: "Paid"
|
||||
"billing.ok": undefined, // In English: "OK"
|
||||
"billing.resume_subscription": undefined, // In English: "Resume Subscription"
|
||||
"billing.subscription_cancelled": undefined, // In English: "Your subscription has been canceled."
|
||||
"billing.subscription_cancelled_description": undefined, // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
"billing.offering.free": undefined, // In English: "Free"
|
||||
"billing.offering.pro": undefined, // In English: "Professional"
|
||||
"billing.offering.business": undefined, // In English: "Business"
|
||||
"billing.cloud_storage": undefined, // In English: "Cloud Storage"
|
||||
"billing.ai_access": undefined, // In English: "AI Access"
|
||||
"billing.bandwidth": undefined, // In English: "Bandwidth"
|
||||
"billing.apps_and_games": undefined, // In English: "Apps & Games"
|
||||
"billing.upgrade_to_pro": undefined, // In English: "Upgrade to %strong%"
|
||||
"billing.switch_to": undefined, // In English: "Switch to %strong%"
|
||||
"billing.payment_setup": undefined, // In English: "Payment Setup"
|
||||
"billing.back": undefined, // In English: "Back"
|
||||
"billing.you_are_now_subscribed_to": undefined, // In English: "You are now subscribed to %strong% tier."
|
||||
"billing.you_are_now_subscribed_to_without_tier": undefined, // In English: "You are now subscribed"
|
||||
"billing.subscription_cancellation_confirmation": undefined, // In English: "Are you sure you want to cancel your subscription?"
|
||||
"billing.subscription_setup": undefined, // In English: "Subscription Setup"
|
||||
"billing.cancel_it": undefined, // In English: "Cancel It"
|
||||
"billing.keep_it": undefined, // In English: "Keep It"
|
||||
"billing.subscription_resumed": undefined, // In English: "Your %strong% subscription has been resumed!"
|
||||
"billing.upgrade_now": undefined, // In English: "Upgrade Now"
|
||||
"billing.upgrade": undefined, // In English: "Upgrade"
|
||||
"billing.currently_on_free_plan": undefined, // In English: "You are currently on the free plan."
|
||||
"billing.download_receipt": undefined, // In English: "Download Receipt"
|
||||
"billing.subscription_check_error": undefined, // In English: "A problem occurred while checking your subscription status."
|
||||
"billing.email_confirmation_needed": undefined, // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
"billing.sub_cancelled_but_valid_until": undefined, // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
"billing.current_plan_until_end_of_period": undefined, // In English: "Your current plan until the end of this billing period."
|
||||
"billing.current_plan": undefined, // In English: "Current plan"
|
||||
"billing.cancelled_subscription_tier": undefined, // In English: "Cancelled Subscription (%%)"
|
||||
"billing.manage": undefined, // In English: "Manage"
|
||||
"billing.limited": undefined, // In English: "Limited"
|
||||
"billing.expanded": undefined, // In English: "Expanded"
|
||||
"billing.accelerated": undefined, // In English: "Accelerated"
|
||||
"billing.enjoy_msg": undefined, // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
change: 'cambiar', // In English: "Change"
|
||||
clock_visibility: 'visibilidadReloj', // In English: "Clock Visibility"
|
||||
reading: 'lectura %strong%', // In English: "Reading %strong%"
|
||||
writing: 'escribiendo %strong%', // In English: "Writing %strong%"
|
||||
unzipping: 'descomprimiendo %strong%', // In English: "Unzipping %strong%"
|
||||
sequencing: 'secuenciación %strong%', // In English: "Sequencing %strong%"
|
||||
zipping: 'comprimiendo %strong%', // In English: "Zipping %strong%"
|
||||
Editor: 'Editor', // In English: "Editor"
|
||||
Viewer: 'Espectador', // In English: "Viewer"
|
||||
'People with access': 'Personas con acceso', // In English: "People with access"
|
||||
'Share With…': 'Compartir con…', // In English: "Share With…"
|
||||
Owner: 'Propietario', // In English: "Owner"
|
||||
"You can't share with yourself.": 'No puedes compartir contigo mismo.', // In English: "You can't share with yourself."
|
||||
'This user already has access to this item':
|
||||
'Este usuario ya tiene acceso a este elemento.', // In English: "This user already has access to this item"
|
||||
|
||||
}
|
||||
};
|
||||
// === Billing ===
|
||||
|
||||
export default es;
|
||||
'billing.change_payment_method': 'Cambiar método de pago', // In English: "Change Payment Method"
|
||||
'billing.cancel': 'Cancelar', // In English: "Cancel"
|
||||
'billing.download_invoice': 'Descargar factura', // In English: "Download Invoice"
|
||||
'billing.payment_method': 'Método de pago', // In English: "Payment Method"
|
||||
'billing.payment_method_updated': '¡Método de pago actualizado!', // In English: "Payment method updated!"
|
||||
'billing.confirm_payment_method': 'Confirmar método de pago', // In English: "Confirm Payment Method"
|
||||
'billing.payment_history': 'Historial de pagos', // In English: "Payment History"
|
||||
'billing.refunded': 'Reembolsado', // In English: "Refunded"
|
||||
'billing.paid': 'Pagado', // In English: "Paid"
|
||||
'billing.ok': 'Aceptar', // In English: "OK"
|
||||
'billing.resume_subscription': 'Reanudar suscripción', // In English: "Resume Subscription"
|
||||
'billing.subscription_cancelled': 'Tu suscripción ha sido cancelada.', // In English: "Your subscription has been canceled."
|
||||
'billing.subscription_cancelled_description':
|
||||
'Aún tendrás acceso a tu suscripción hasta el final de este periodo de facturación.', // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
'billing.offering.free': 'Gratis', // In English: "Free"
|
||||
'billing.offering.pro': 'Profesional', // In English: "Professional"
|
||||
'billing.offering.business': 'Negocios', // In English: "Business"
|
||||
'billing.cloud_storage': 'Almacenamiento en la nube', // In English: "Cloud Storage"
|
||||
'billing.ai_access': 'Acceso a IA', // In English: "AI Access"
|
||||
'billing.bandwidth': 'Ancho de banda', // In English: "Bandwidth"
|
||||
'billing.apps_and_games': 'Aplicaciones y juegos', // In English: "Apps & Games"
|
||||
'billing.upgrade_to_pro': 'Actualizar a %strong%', // In English: "Upgrade to %strong%"
|
||||
'billing.switch_to': 'Cambiar a %strong%', // In English: "Switch to %strong%"
|
||||
'billing.payment_setup': 'Configuración de pago', // In English: "Payment Setup"
|
||||
'billing.back': 'Atrás', // In English: "Back"
|
||||
'billing.you_are_now_subscribed_to':
|
||||
'Ahora estás suscrito al nivel %strong%.', // In English: "You are now subscribed to %strong% tier."
|
||||
'billing.you_are_now_subscribed_to_without_tier': 'Ahora estás suscrito', // In English: "You are now subscribed"
|
||||
'billing.subscription_cancellation_confirmation':
|
||||
'¿Estás seguro de que deseas cancelar tu suscripción?', // In English: "Are you sure you want to cancel your subscription?"
|
||||
'billing.subscription_setup': 'Configuración de suscripción', // In English: "Subscription Setup"
|
||||
'billing.cancel_it': 'Cancelar', // In English: "Cancel It"
|
||||
'billing.keep_it': 'Mantenerlo', // In English: "Keep It"
|
||||
'billing.subscription_resumed':
|
||||
'¡Tu suscripción %strong% ha sido reanudada!', // In English: "Your %strong% subscription has been resumed!"
|
||||
'billing.upgrade_now': 'Actualizar ahora', // In English: "Upgrade Now"
|
||||
'billing.upgrade': 'Actualizar', // In English: "Upgrade"
|
||||
'billing.currently_on_free_plan': 'Actualmente estás en el plan gratuito.', // In English: "You are currently on the free plan."
|
||||
'billing.download_receipt': 'Descargar recibo', // In English: "Download Receipt"
|
||||
'billing.subscription_check_error':
|
||||
'Ocurrió un problema al verificar el estado de tu suscripción.', // In English: "A problem occurred while checking your subscription status."
|
||||
'billing.email_confirmation_needed':
|
||||
'Tu correo electrónico no ha sido confirmado. Te enviaremos un código para confirmarlo ahora.', // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
'billing.sub_cancelled_but_valid_until':
|
||||
'Has cancelado tu suscripción y se cambiará automáticamente al nivel gratuito al final del periodo de facturación. No se te cobrará nuevamente a menos que te vuelvas a suscribir.', // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
'billing.current_plan_until_end_of_period':
|
||||
'Tu plan actual hasta el final de este periodo de facturación.', // In English: "Your current plan until the end of this billing period."
|
||||
'billing.current_plan': 'Plan actual', // In English: "Current plan"
|
||||
'billing.cancelled_subscription_tier': 'Suscripción cancelada (%%)', // In English: "Cancelled Subscription (%%)"
|
||||
'billing.manage': 'Gestionar', // In English: "Manage"
|
||||
'billing.limited': 'Limitado', // In English: "Limited"
|
||||
'billing.expanded': 'Expandido', // In English: "Expanded"
|
||||
'billing.accelerated': 'Acelerado', // In English: "Accelerated"
|
||||
'billing.enjoy_msg':
|
||||
'Disfruta %% de almacenamiento en la nube junto con otros beneficios.', // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
},
|
||||
}
|
||||
|
||||
export default es
|
||||
|
@ -334,7 +334,7 @@ const hu = {
|
||||
login2fa_recovery_back: "Vissza",
|
||||
login2fa_recovery_placeholder: "XXXXXXXX",
|
||||
|
||||
"change": "váltás", // In English: "Change"
|
||||
"change": "Módosítás", // In English: "Change"
|
||||
"clock_visibility": "Óra Megjelenítése", // In English: "Clock Visibility"
|
||||
"reading": "Olvasás %strong%", // In English: "Reading %strong%"
|
||||
"writing": "Írás %strong%", // In English: "Writing %strong%"
|
||||
@ -349,56 +349,54 @@ const hu = {
|
||||
"You can't share with yourself.": "Nem oszthatod meg magaddal.", // In English: "You can't share with yourself."
|
||||
"This user already has access to this item": "Ez a felhasználó már hozzáfér ehhez az elemhez", // In English: "This user already has access to this item"
|
||||
|
||||
// ----------------------------------------
|
||||
// Missing translations:
|
||||
// ----------------------------------------
|
||||
"billing.change_payment_method": undefined, // In English: "Change"
|
||||
"billing.cancel": undefined, // In English: "Cancel"
|
||||
"billing.download_invoice": undefined, // In English: "Download"
|
||||
"billing.payment_method": undefined, // In English: "Payment Method"
|
||||
"billing.payment_method_updated": undefined, // In English: "Payment method updated!"
|
||||
"billing.confirm_payment_method": undefined, // In English: "Confirm Payment Method"
|
||||
"billing.payment_history": undefined, // In English: "Payment History"
|
||||
"billing.refunded": undefined, // In English: "Refunded"
|
||||
"billing.paid": undefined, // In English: "Paid"
|
||||
"billing.ok": undefined, // In English: "OK"
|
||||
"billing.resume_subscription": undefined, // In English: "Resume Subscription"
|
||||
"billing.subscription_cancelled": undefined, // In English: "Your subscription has been canceled."
|
||||
"billing.subscription_cancelled_description": undefined, // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
"billing.offering.free": undefined, // In English: "Free"
|
||||
"billing.offering.pro": undefined, // In English: "Professional"
|
||||
"billing.offering.business": undefined, // In English: "Business"
|
||||
"billing.cloud_storage": undefined, // In English: "Cloud Storage"
|
||||
"billing.ai_access": undefined, // In English: "AI Access"
|
||||
"billing.bandwidth": undefined, // In English: "Bandwidth"
|
||||
"billing.apps_and_games": undefined, // In English: "Apps & Games"
|
||||
"billing.upgrade_to_pro": undefined, // In English: "Upgrade to %strong%"
|
||||
"billing.switch_to": undefined, // In English: "Switch to %strong%"
|
||||
"billing.payment_setup": undefined, // In English: "Payment Setup"
|
||||
"billing.back": undefined, // In English: "Back"
|
||||
"billing.you_are_now_subscribed_to": undefined, // In English: "You are now subscribed to %strong% tier."
|
||||
"billing.you_are_now_subscribed_to_without_tier": undefined, // In English: "You are now subscribed"
|
||||
"billing.subscription_cancellation_confirmation": undefined, // In English: "Are you sure you want to cancel your subscription?"
|
||||
"billing.subscription_setup": undefined, // In English: "Subscription Setup"
|
||||
"billing.cancel_it": undefined, // In English: "Cancel It"
|
||||
"billing.keep_it": undefined, // In English: "Keep It"
|
||||
"billing.subscription_resumed": undefined, // In English: "Your %strong% subscription has been resumed!"
|
||||
"billing.upgrade_now": undefined, // In English: "Upgrade Now"
|
||||
"billing.upgrade": undefined, // In English: "Upgrade"
|
||||
"billing.currently_on_free_plan": undefined, // In English: "You are currently on the free plan."
|
||||
"billing.download_receipt": undefined, // In English: "Download Receipt"
|
||||
"billing.subscription_check_error": undefined, // In English: "A problem occurred while checking your subscription status."
|
||||
"billing.email_confirmation_needed": undefined, // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
"billing.sub_cancelled_but_valid_until": undefined, // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
"billing.current_plan_until_end_of_period": undefined, // In English: "Your current plan until the end of this billing period."
|
||||
"billing.current_plan": undefined, // In English: "Current plan"
|
||||
"billing.cancelled_subscription_tier": undefined, // In English: "Cancelled Subscription (%%)"
|
||||
"billing.manage": undefined, // In English: "Manage"
|
||||
"billing.limited": undefined, // In English: "Limited"
|
||||
"billing.expanded": undefined, // In English: "Expanded"
|
||||
"billing.accelerated": undefined, // In English: "Accelerated"
|
||||
"billing.enjoy_msg": undefined, // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
|
||||
"billing.change_payment_method": "Módosítás", // In English: "Change"
|
||||
"billing.cancel": "Lemondás", // In English: "Cancel"
|
||||
"billing.change_payment_method": "Fizetési mód megváltoztatása", // In English: "Change"
|
||||
"billing.cancel": "Mégse", // In English: "Cancel"
|
||||
"billing.download_invoice": "Letöltés", // In English: "Download"
|
||||
"billing.payment_method": "Fizetési mód", // In English: "Payment Method"
|
||||
"billing.payment_method_updated": "A fizetési mód frissítve!", // In English: "Payment method updated!"
|
||||
"billing.confirm_payment_method": "Fizetési mód megerősítése", // In English: "Confirm Payment Method"
|
||||
"billing.payment_history": "Fizetési előzmények", // In English: "Payment History"
|
||||
"billing.refunded": "Visszatérítve", // In English: "Refunded"
|
||||
"billing.paid": "Fizetve", // In English: "Paid"
|
||||
"billing.ok": "OK", // In English: "OK"
|
||||
"billing.resume_subscription": "Előfizetés folytatása", // In English: "Resume Subscription"
|
||||
"billing.subscription_cancelled": "Az előfizetésed lemondva.", // In English: "Your subscription has been canceled."
|
||||
"billing.subscription_cancelled_description": "Az aktuális számlázási időszak végéig továbbra is hozzáférsz az előfizetésedhez.", // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
"billing.offering.free": "Ingyenes", // In English: "Free"
|
||||
"billing.offering.pro": "Professzionális", // In English: "Professional"
|
||||
"billing.offering.business": "Üzleti", // In English: "Business"
|
||||
"billing.cloud_storage": "Felhő Tárhely", // In English: "Cloud Storage"
|
||||
"billing.ai_access": "AI hozzáférés", // In English: "AI Access"
|
||||
"billing.bandwidth": "Sávszélesség", // In English: "Bandwidth"
|
||||
"billing.apps_and_games": "Alkalmazások és játékok", // In English: "Apps & Games"
|
||||
"billing.upgrade_to_pro": "Csomag váltása erre: %strong%", // In English: "Upgrade to %strong%"
|
||||
"billing.switch_to": "Váltás erre: %strong%", // In English: "Switch to %strong%"
|
||||
"billing.payment_setup": "Fizetési beállítás", // In English: "Payment Setup"
|
||||
"billing.back": "Vissza", // In English: "Back"
|
||||
"billing.you_are_now_subscribed_to": "Mostantól feliratkoztál a %strong% csomagra.", // In English: "You are now subscribed to %strong% tier."
|
||||
"billing.you_are_now_subscribed_to_without_tier": "Mostantól előfizettél", // In English: "You are now subscribed"
|
||||
"billing.subscription_cancellation_confirmation": "Biztos, hogy le akarod mondani az előfizetésedet?", // In English: "Are you sure you want to cancel your subscription?"
|
||||
"billing.subscription_setup": "Előfizetés beállítása", // In English: "Subscription Setup"
|
||||
"billing.cancel_it": "Mondd le", // In English: "Cancel It"
|
||||
"billing.keep_it": "Tartsd meg", // In English: "Keep It"
|
||||
"billing.subscription_resumed": "A(z) %strong% előfizetésed folytatva lett!", // In English: "Your %strong% subscription has been resumed!"
|
||||
"billing.upgrade_now": "Válts magasabb csomagra most", // In English: "Upgrade Now"
|
||||
"billing.upgrade": "Csomag Váltása", // In English: "Upgrade"
|
||||
"billing.currently_on_free_plan": "Jelenleg az ingyenes csomagon vagy.", // In English: "You are currently on the free plan."
|
||||
"billing.download_receipt": "Nyugta letöltése", // In English: "Download Receipt"
|
||||
"billing.subscription_check_error": "Hiba történt az előfizetési állapot ellenőrzése közben.", // In English: "A problem occurred while checking your subscription status."
|
||||
"billing.email_confirmation_needed": "Az e-mail címed még nincs megerősítve. Most küldünk egy kódot a megerősítéshez.", // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
"billing.sub_cancelled_but_valid_until": "Lemondtad az előfizetésedet, és a számlázási időszak végén automatikusan az ingyenes csomagra vált. Nem fogsz újra díjat fizetni, hacsak nem fizetsz elő újra.", // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
"billing.current_plan_until_end_of_period": "Az aktuális csomagod a számlázási időszak végéig.", // In English: "Your current plan until the end of this billing period."
|
||||
"billing.current_plan": "Jelenlegi csomag", // In English: "Current plan"
|
||||
"billing.cancelled_subscription_tier": "Lemondott Előfizetés (%%)", // In English: "Cancelled Subscription (%%)"
|
||||
"billing.manage": "Kezelés", // In English: "Manage"
|
||||
"billing.limited": "Korlátozott", // In English: "Limited"
|
||||
"billing.expanded": "Bővített", // In English: "Expanded"
|
||||
"billing.accelerated": "Gyorsított", // In English: "Accelerated"
|
||||
"billing.enjoy_msg": "Élvezd a %% Felhőtárhelyet és további előnyöket.", // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -17,6 +17,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Translator note: It depends on the overall tone you are going for, however I would suggest changing verbs ending in 십시오 to 세요 for a more modern, user-friendly tone that aligns with conversational UI trends.
|
||||
// Translator note: 십시오 is very formal and better suited for official or enterprise contexts (e.g government websites), while 세요 feels approachable and appropriate for general users.
|
||||
// Translator note: I have noted down improvement suggestions below based on my knowledge of modern Korean user interfaces without changing the original translations for your reference.
|
||||
|
||||
const ko = {
|
||||
name: "한국어",
|
||||
english_name: "Korean",
|
||||
@ -24,7 +28,7 @@ const ko = {
|
||||
dictionary: {
|
||||
about: "정보",
|
||||
account: "계정",
|
||||
account_password: "",
|
||||
account_password: "계정 비밀번호", // Added translation: "account password"
|
||||
access_granted_to: "접근 권한 부여",
|
||||
add_existing_account: "기존 계정 추가",
|
||||
all_fields_required: "모든 항목은 필수 입력사항입니다.",
|
||||
@ -53,29 +57,29 @@ const ko = {
|
||||
confirm_2fa_setup: "코드를 인증 앱에 추가했습니다",
|
||||
confirm_2fa_recovery: "복구 코드를 안전한 위치에 저장했습니다",
|
||||
confirm_account_for_free_referral_storage_c2a:
|
||||
"계정을 생성하고 이메일 주소를 확인하여 1GB의 무료 저장 공간을 받으십시오. 친구도 1GB의 무료 저장 공간을 받게 됩니다.",
|
||||
"계정을 생성하고 이메일 주소를 확인하여 1GB의 무료 저장 공간을 받으십시오. 친구도 1GB의 무료 저장 공간을 받게 됩니다.", // Improvement suggestion: "계정을 만들고 이메일 주소를 확인하면 1GB의 무료 저장 공간을 드립니다! 친구와 공유하면 친구도 1GB를 받을 수 있습니다."
|
||||
confirm_code_generic_incorrect: "잘못된 코드입니다.",
|
||||
confirm_code_generic_too_many_requests:
|
||||
"요청이 너무 많습니다. 몇 분만 기다려주십시오.",
|
||||
"요청이 너무 많습니다. 몇 분만 기다려주십시오.", // Improvement suggestion: "요청이 너무 많습니다. 잠시만 기다려주세요."
|
||||
confirm_code_generic_submit: "코드 제출",
|
||||
confirm_code_generic_try_again: "재시도",
|
||||
confirm_code_generic_title: "확인 코드를 입력하십시오",
|
||||
confirm_code_2fa_instruction: "인증 앱의 6자리 코드를 입력하십시오.",
|
||||
confirm_code_generic_title: "확인 코드를 입력하십시오", // Improvement suggestion: "인증 코드를 입력해주세요." (use 인증 if it's a verification/confirmation code)
|
||||
confirm_code_2fa_instruction: "인증 앱의 6자리 코드를 입력하십시오.", // Improvement suggestion: "인증 앱의 6자리 코드를 입력해주세요."
|
||||
confirm_code_2fa_submit_btn: "제출",
|
||||
confirm_code_2fa_title: "2FA 코드를 입력하십시오",
|
||||
confirm_code_2fa_title: "2FA 코드를 입력하십시오", // Improvement suggestion: "2FA 코드를 입력해주세요."
|
||||
confirm_delete_multiple_items:
|
||||
"정말로 이 항목들을 영구적으로 삭제하시겠습니까?",
|
||||
"정말로 이 항목들을 영구적으로 삭제하시겠습니까?", // Improvement suggestion: "항목들을 정말 영구적으로 삭제하시겠습니까?" (if it's a selection you could add "selected" to items like this: "선택된 항목들을 정말 영구적으로 삭제하시겠습니까?")
|
||||
confirm_delete_single_item: "이 항목을 영구적으로 삭제하시겠습니까?",
|
||||
confirm_open_apps_log_out:
|
||||
"열려있는 앱들이 있습니다. 정말로 로그아웃 하시겠습니까?",
|
||||
confirm_new_password: "새 비밀번호 확인",
|
||||
confirm_delete_user:
|
||||
"정말로 계정을 삭제하시겠습니까? 모든 파일과 데이터가 영구적으로 삭제됩니다. 이 작업은 취소될 수 없습니다.",
|
||||
"정말로 계정을 삭제하시겠습니까? 모든 파일과 데이터가 영구적으로 삭제됩니다. 이 작업은 취소될 수 없습니다.", // Improvement suggestion: "정말 계정을 삭제하시겠습니까? 모든 파일과 데이터가 영구적으로 삭제되며, 이 작업은 취소할 수 없습니다."
|
||||
confirm_delete_user_title: "계정 삭제?",
|
||||
confirm_session_revoke: "정말로 이 세션을 취소하시겠습니까?",
|
||||
confirm_your_email_address: "이메일 주소를 확인하십시오",
|
||||
confirm_your_email_address: "이메일 주소를 확인하십시오", // Improvement suggestion: "이메일 주소를 확인해주세요."
|
||||
contact_us: "문의하기",
|
||||
contact_us_verification_required: "인증된 이메일 주소가 있어야 합니다.",
|
||||
contact_us_verification_required: "인증된 이메일 주소가 있어야 합니다.", // Improvement suggestion: "이메일 인증이 필요합니다."
|
||||
contain: "포함",
|
||||
continue: "계속",
|
||||
copy: "복사",
|
||||
@ -109,7 +113,7 @@ const ko = {
|
||||
dir_published_as_website: `%strong% 다음에 게시되었습니다:`,
|
||||
disable_2fa: "2FA 비활성화",
|
||||
disable_2fa_confirm: "정말로 2FA를 비활성화 하시겠습니까?",
|
||||
disable_2fa_instructions: "2FA 비활성화를 하려면 비밀번호를 입력하십시오.",
|
||||
disable_2fa_instructions: "2FA 비활성화를 하려면 비밀번호를 입력하십시오.", // Improvement suggestion: "2FA 비활성화를 하려면 비밀번호를 입력해주세요."
|
||||
disassociate_dir: "디렉토리 연결 해제",
|
||||
documents: "문서",
|
||||
dont_allow: "허용하지 않음",
|
||||
@ -118,7 +122,7 @@ const ko = {
|
||||
downloading: "다운로드 중",
|
||||
email: "이메일",
|
||||
email_change_confirmation_sent:
|
||||
"새 이메일 주소로 확인 메일이 전송되었습니다. 받은 편지함을 확인하시고 안내에 따라 절차를 완료하십시오.",
|
||||
"새 이메일 주소로 확인 메일이 전송되었습니다. 받은 편지함을 확인하시고 안내에 따라 절차를 완료하십시오.", // Improvement suggestion: "새 이메일 주소로 확인 메일이 전송되었습니다. 받은 편지함을 확인 후 안내에 따라 절차를 완료해주세요."
|
||||
email_invalid: "이메일이 유효하지 않습니다.",
|
||||
email_or_username: "이메일 또는 사용자 이름",
|
||||
email_required: "이메일은 필수 입력사항입니다.",
|
||||
@ -131,16 +135,16 @@ const ko = {
|
||||
end_soft: "소프트 종료",
|
||||
enlarged_qr_code: "확대된 QR 코드",
|
||||
enter_password_to_confirm_delete_user:
|
||||
"계정 삭제를 승인하려면 비밀번호를 입력하십시오.",
|
||||
"계정 삭제를 승인하려면 비밀번호를 입력하십시오.", // Improvement suggestion: "계정 삭제를 승인하려면 비밀번호를 입력해주세요."
|
||||
error_message_is_missing: "오류 메세지를 찾을 수 없습니다.",
|
||||
error_unknown_cause: "알 수 없는 오류가 발생했습니다.",
|
||||
error_uploading_files: "파일들을 업로드 하는데 실패했습니다",
|
||||
error_uploading_files: "파일들을 업로드 하는데 실패했습니다", // Improvement suggestion: "파일 업로드가 실패했습니다"
|
||||
favorites: "즐겨찾기",
|
||||
feedback: "피드백",
|
||||
feedback_c2a:
|
||||
"아래 양식을 사용하여 피드백, 의견 및 버그 보고를 보내십시오.",
|
||||
"아래 양식을 사용하여 피드백, 의견 및 버그 보고를 보내십시오.", // Improvement suggestion: "아래 양식을 통해 피드백, 의견 또는 버그 보고를 보내주세요."
|
||||
feedback_sent_confirmation:
|
||||
"문의해 주셔서 감사합니다. 계정에 이메일이 연결되어 있으면 가능한 빨리 회신 드리겠습니다.",
|
||||
"문의해 주셔서 감사합니다. 계정에 이메일이 연결되어 있으면 가능한 빨리 회신 드리겠습니다.", // Improvement suggestion: "문의해 주셔서 감사합니다. 계정에 이메일이 연결되어 있다면 최대한 빨리 답변드리겠습니다."
|
||||
fit: "맞춤",
|
||||
folder: "폴더",
|
||||
force_quit: "강제 종료",
|
||||
@ -157,7 +161,7 @@ const ko = {
|
||||
incorrect_password: "잘못된 비밀번호",
|
||||
invite_link: "초대 링크",
|
||||
item: "개 항목",
|
||||
items_in_trash_cannot_be_renamed: `이 항목은 휴지통에 있기 때문에 이름을 바꿀 수 없습니다. 이 항목의 이름을 바꾸려면 먼저 휴지통에서 끌어내십시오.`,
|
||||
items_in_trash_cannot_be_renamed: `이 항목은 휴지통에 있기 때문에 이름을 바꿀 수 없습니다. 이 항목의 이름을 바꾸려면 먼저 휴지통에서 끌어내십시오.`, // Improvement suggestion: "이 항목은 휴지통에 있어 이름을 변경할 수 없습니다. 이름을 변경하려면 먼저 휴지통에서 복원해주세요."
|
||||
jpeg_image: "JPEG 이미지",
|
||||
keep_in_taskbar: "작업 표시줄에 유지",
|
||||
language: "언어",
|
||||
@ -204,11 +208,11 @@ const ko = {
|
||||
password: "비밀번호",
|
||||
password_changed: "비밀번호가 변경되었습니다.",
|
||||
password_recovery_rate_limit:
|
||||
"속도 제한에 도달했습니다. 몇 분만 기다려 주십시오. 앞으로 이 문제를 방지하려면 페이지를 너무 많이 다시 로드하지 마십시오.",
|
||||
"속도 제한에 도달했습니다. 몇 분만 기다려 주십시오. 앞으로 이 문제를 방지하려면 페이지를 너무 많이 다시 로드하지 마십시오.", // Improvement suggestion: "속도 제한에 도달했습니다. 잠시만 기다려주세요. 앞으로 이런 문제가 발생하지 않도록 페이지를 자주 새로고침하지 마세요."
|
||||
password_recovery_token_invalid:
|
||||
"이 비밀번호 복구 토큰은 더 이상 유효하지 않습니다.",
|
||||
"이 비밀번호 복구 토큰은 더 이상 유효하지 않습니다.", // Improvement suggestion: "유효하지 않은 비밀번호 복구 토큰입니다."
|
||||
password_recovery_unknown_error:
|
||||
"알 수 없는 오류가 발생했습니다. 나중에 다시 시도해주십시오.",
|
||||
"알 수 없는 오류가 발생했습니다. 나중에 다시 시도해주십시오.", // Improvement suggestion: "알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요."
|
||||
password_required: "비밀번호는 필수 입력사항 입니다.",
|
||||
password_strength_error:
|
||||
"비밀번호는 반드시 최소 8자 이상이어야 하며 최소 대문자 1개, 소문자 1개, 숫자 1개, 특수문자 1개를 포함해야 합니다.",
|
||||
@ -244,8 +248,8 @@ const ko = {
|
||||
recommended: "추천",
|
||||
recover_password: "비밀번호 찾기",
|
||||
refer_friends_c2a:
|
||||
"Puter에서 계정을 생성하고 확인한 친구마다 1GB를 받으십시오. 친구도 1GB를 받게 됩니다!",
|
||||
refer_friends_social_media_c2a: `Puter.com에서 1GB의 무료 저장 공간을 받으십시오!`,
|
||||
"Puter에서 계정을 생성하고 확인한 친구마다 1GB를 받으십시오. 친구도 1GB를 받게 됩니다!", // Improvement suggestion: "Puter에서 계정을 만들고 확인한 친구마다 1GB를 받아보세요. 친구도 1GB를 받게 됩니다!"
|
||||
refer_friends_social_media_c2a: `Puter.com에서 1GB의 무료 저장 공간을 받으십시오!`, // Improvement suggestion: "Puter.com에서 1GB의 무료 저장 공간을 받아보세요!"
|
||||
refresh: "새로 고침",
|
||||
release_address_confirmation: `이 주소를 해제하시겠습니까?`,
|
||||
remove_from_taskbar: "작업 표시줄에서 제거",
|
||||
@ -253,20 +257,20 @@ const ko = {
|
||||
repeat: "반복",
|
||||
replace: "교체",
|
||||
replace_all: "모두 교체",
|
||||
resend_confirmation_code: "확인 코드 다시 보내기",
|
||||
resend_confirmation_code: "확인 코드 다시 보내기", // Improvement suggestion: "인증 코드 재전송"
|
||||
reset_colors: "색상 초기화",
|
||||
restart_puter_confirm: "정말 Puter를 다시 시작하시겠습니까?",
|
||||
restore: "복원",
|
||||
save: "저장",
|
||||
saturation: "채도",
|
||||
save_account: "계정 저장",
|
||||
save_account_to_get_copy_link: "계속하려면 계정을 생성하십시오.",
|
||||
save_account_to_publish: "계속하려면 계정을 생성하십시오.",
|
||||
save_account_to_get_copy_link: "계속하려면 계정을 생성하십시오.", // Improvement suggestion: "계속하려면 계정을 만들어주세요."
|
||||
save_account_to_publish: "계속하려면 계정을 생성하십시오.", // Improvement suggestion: "계속하려면 계정을 만들어주세요."
|
||||
save_session: "세션 저장",
|
||||
save_session_c2a:
|
||||
"현재 세션을 저장하고 작업을 잃지 않으려면 계정을 생성하십시오.",
|
||||
"현재 세션을 저장하고 작업을 잃지 않으려면 계정을 생성하십시오.", // Improvement suggestion: "현재 세션을 저장하고 작업을 잃지 않으려면 계정을 만들어주세요."
|
||||
scan_qr_c2a:
|
||||
"다른 기기에서 이 세션으로 로그인하려면 아래 코드를 스캔하십시오",
|
||||
"다른 기기에서 이 세션으로 로그인하려면 아래 코드를 스캔하십시오", // Improvement suggestion: change 스캔하십시오 to 스캔해주세요 for the next 3 lines.
|
||||
scan_qr_2fa: "인증 앱으로 QR 코드를 스캔하십시오.",
|
||||
scan_qr_generic: "휴대전화나 다른 기기로 QR 코드를 스캔하십시오",
|
||||
search: "검색",
|
||||
@ -278,7 +282,7 @@ const ko = {
|
||||
sessions: "세션",
|
||||
send: "보내기",
|
||||
send_password_recovery_email: "비밀번호 복구 이메일 보내기",
|
||||
session_saved: "계정을 생성해 주셔서 감사합니다. 이 세션이 저장되었습니다.",
|
||||
session_saved: "계정을 생성해 주셔서 감사합니다. 이 세션이 저장되었습니다.", // Improvement suggestion: "계정을 만들어주셔서 감사합니다. 현재 세션이 저장되었습니다."
|
||||
settings: "설정",
|
||||
set_new_password: "새 비밀번호 설정",
|
||||
share: "공유",
|
||||
@ -292,14 +296,15 @@ const ko = {
|
||||
signing_in: "로그인 중…",
|
||||
size: "크기",
|
||||
skip: "건너뛰기",
|
||||
something_went_wrong: "무언가 잘못되었습니다.",
|
||||
something_went_wrong: "무언가 잘못되었습니다.", // Improvement suggestion: "문제가 발생했습니다" ("There was a problem") which is more widely used for errors.
|
||||
sort_by: "정렬 기준",
|
||||
start: "시작",
|
||||
status: "상태",
|
||||
storage_usage: "저장 공간 사용량",
|
||||
storage_puter_used: "Puter에서 사용 중",
|
||||
taking_longer_than_usual:
|
||||
"보통보다 조금 더 오래 걸립니다. 잠시만 기다려 주십시오...",
|
||||
"보통보다 조금 더 오래 걸립니다. 잠시만 기다려 주십시오...", // Improvement suggestion: "평소보다 조금 더 오래 걸리고 있습니다. 잠시만 기다려주세요..."
|
||||
|
||||
task_manager: "작업 관리자",
|
||||
taskmgr_header_name: "이름",
|
||||
taskmgr_header_status: "상태",
|
||||
@ -314,7 +319,7 @@ const ko = {
|
||||
two_factor_enabled: "2FA 활성화됨",
|
||||
type: "유형",
|
||||
type_confirm_to_delete_account:
|
||||
"계정을 삭제하려면 'confirm'을 입력하십시오.",
|
||||
"계정을 삭제하려면 'confirm'을 입력하십시오.", // Improvement suggestion: "계정을 삭제하려면 'confirm'을 입력해주세요."
|
||||
ui_colors: "UI 색상",
|
||||
ui_manage_sessions: "세션 관리자",
|
||||
ui_revoke: "취소",
|
||||
@ -325,112 +330,117 @@ const ko = {
|
||||
upload_here: "여기에 업로드",
|
||||
usage: "사용량",
|
||||
username: "사용자 이름",
|
||||
username_changed: "사용자 이름이 성공적으로 업데이트되었습니다.",
|
||||
username_changed: "사용자 이름이 성공적으로 업데이트되었습니다.", // Improvement suggestion: "사용자 이름이 변경되었습니다." (simplified)
|
||||
username_required: "사용자 이름은 필수 입력사항입니다.",
|
||||
versions: "버전",
|
||||
videos: "동영상",
|
||||
visibility: "가시성",
|
||||
visibility: "가시성", // This depends on the specific context - if it means that content is visible/public or hidden/private, I would suggest changing it to "공개 여부" if the user can choose Yes/No or simply 공개 for visible/public) and 비공개 for invisible/private.
|
||||
yes: "예",
|
||||
yes_release_it: "예, 해제합니다",
|
||||
you_have_been_referred_to_puter_by_a_friend: "친구가 Puter로 추천했습니다!",
|
||||
you_have_been_referred_to_puter_by_a_friend: "친구가 Puter로 추천했습니다!", // Improvement suggestion: "친구가 Puter를 추천했습니다!"
|
||||
zip: "압축",
|
||||
zipping_file: "%strong% 압축 중",
|
||||
|
||||
// === 2FA Setup ===
|
||||
setup2fa_1_step_heading: "인증 앱을 여십시오",
|
||||
setup2fa_1_step_heading: "인증 앱을 여십시오", // Improvement suggestion: "인증 앱을 열어주세요."
|
||||
setup2fa_1_instructions: `
|
||||
시간 기반 일회용 비밀번호(TOTP) 프로토콜을 지원하는 모든 인증 앱을 사용할 수 있습니다.
|
||||
선택할 수 있는 앱은 많지만, 잘 모르겠다면 안드로이드 및 iOS용
|
||||
<a target="_blank" href="https://authy.com/download">Authy</a>
|
||||
가 무난한 선택입니다.
|
||||
`,
|
||||
setup2fa_2_step_heading: "QR 코드를 스캔하십시오",
|
||||
setup2fa_3_step_heading: "6자리 코드를 입력하십시오",
|
||||
setup2fa_4_step_heading: "복구 코드를 복사하십시오",
|
||||
setup2fa_2_step_heading: "QR 코드를 스캔하십시오", // Improvement suggestion: "QR 코드를 스캔해주세요"
|
||||
setup2fa_3_step_heading: "6자리 코드를 입력하십시오", // Improvement suggestion: "6자리 코드를 입력해주세요"
|
||||
setup2fa_4_step_heading: "복구 코드를 복사하십시오", // Improvement suggestion: "복구 코드를 복사해주세요"
|
||||
setup2fa_4_instructions: `
|
||||
이 복구코드들은 휴대전화를 잃어버리거나 인증 앱을 사용할 수 없을 때 계정에 접속할 수 있는 유일한 수단입니다.
|
||||
반드시 안전한 장소에 보관하세요.
|
||||
`,
|
||||
`, // Improvement suggestion: "복구 코드는 휴대전화를 분실하거나 인증 앱을 사용할 수 없을 때 계정에 접속할 수 있는 유일한 방법입니다. 반드시 안전한 장소에 보관하세요."
|
||||
setup2fa_5_step_heading: "2FA 설정 확인",
|
||||
setup2fa_5_confirmation_1: "복구 코드를 안전한 위치에 저장했습니다",
|
||||
setup2fa_5_confirmation_2: "2FA를 활성화할 준비가 되었습니다",
|
||||
setup2fa_5_button: "2FA 활성화",
|
||||
|
||||
// === 2FA Login ===
|
||||
login2fa_otp_title: "2FA 코드를 입력하십시오",
|
||||
login2fa_otp_instructions: "인증 앱의 6자리 코드를 입력하십시오.",
|
||||
login2fa_recovery_title: "복구코드를 입력하십시오",
|
||||
login2fa_otp_title: "2FA 코드를 입력하십시오", // Improvement suggestion: "2FA 코드를 입력해주세요"
|
||||
login2fa_otp_instructions: "인증 앱의 6자리 코드를 입력하십시오.", // Improvement suggestion: "인증 앱의 6자리 코드를 입력해주세요."
|
||||
login2fa_recovery_title: "복구코드를 입력하십시오", // Improvement suggestion: "복구코드를 입력해주세요"
|
||||
login2fa_recovery_instructions:
|
||||
"계정 접속을 위해 복구코드들 중 하나를 입력하십시오.",
|
||||
"계정 접속을 위해 복구코드들 중 하나를 입력하십시오.", // Improvement suggestion: "계정에 접속하려면 복구코드 중 하나를 입력해주세요."
|
||||
login2fa_use_recovery_code: "복구코드 사용",
|
||||
login2fa_recovery_back: "뒤로 가기",
|
||||
login2fa_recovery_back: "뒤로 가기", // Improvement suggestion: "뒤로"
|
||||
login2fa_recovery_placeholder: "XXXXXXXX",
|
||||
|
||||
"account_password": "계정 비밀번호 인증",
|
||||
"change": "변경",
|
||||
"clock_visibility": "시계 표시 설정",
|
||||
"reading": `%strong% 읽는 중`,
|
||||
"writing": `%strong% 기록 중`,
|
||||
"unzipping": `%strong% 압축 해제 중`,
|
||||
"sequencing": `%strong% 순서 처리 중`,
|
||||
"zipping": `%strong% 압축 중`,
|
||||
"Editor": "편집자", // If it refers to a person, the correct translation is "편집자" ,If it refers to the tool or software, the translation would be "편집기"
|
||||
"Viewer": "조회자", // If it refers to a person, the correct translation is "조회자" ,If it refers to the tool or software, the translation would be "뷰어"
|
||||
"People with access": "권한 보유자",
|
||||
account_password: "계정 비밀번호 인증",
|
||||
change: "변경",
|
||||
clock_visibility: "시계 표시 설정",
|
||||
reading: `%strong% 읽는 중`,
|
||||
writing: `%strong% 기록 중`,
|
||||
unzipping: `%strong% 압축 해제 중`,
|
||||
sequencing: `%strong% 순서 처리 중`,
|
||||
zipping: `%strong% 압축 중`,
|
||||
Editor: "편집자", // If it refers to a person, the correct translation is "편집자" ,If it refers to the tool or software, the translation would be "편집기"
|
||||
Viewer: "조회자", // If it refers to a person, the correct translation is "조회자" ,If it refers to the tool or software, the translation would be "뷰어"
|
||||
"People with access": "권한 보유자",
|
||||
"Share With…": "공유 대상...",
|
||||
"Owner": "소유자",
|
||||
"You can't share with yourself.": "자기 자신과는 공유할 수 없습니다.",
|
||||
"This user already has access to this item": "이 사용자는 이미 접근 권한이 있습니다.",
|
||||
Owner: "소유자",
|
||||
"You can't share with yourself.": "자기 자신과는 공유할 수 없습니다.",
|
||||
"This user already has access to this item":
|
||||
"이 사용자는 이미 접근 권한이 있습니다.",
|
||||
|
||||
// ----------------------------------------
|
||||
// Missing translations:
|
||||
// ----------------------------------------
|
||||
"billing.change_payment_method": "변경",
|
||||
"billing.change_payment_method": "결제 수단 변경", // added "payment method"
|
||||
"billing.cancel": "취소",
|
||||
"billing.download_invoice": "다운로드",
|
||||
"billing.payment_method": "결제 방법",
|
||||
"billing.payment_method_updated": "결제 방법 업데이트!",
|
||||
"billing.confirm_payment_method": undefined, // In English: "Confirm Payment Method"
|
||||
"billing.payment_history": undefined, // In English: "Payment History"
|
||||
"billing.refunded": undefined, // In English: "Refunded"
|
||||
"billing.paid": undefined, // In English: "Paid"
|
||||
"billing.ok": undefined, // In English: "OK"
|
||||
"billing.resume_subscription": undefined, // In English: "Resume Subscription"
|
||||
"billing.subscription_cancelled": undefined, // In English: "Your subscription has been canceled."
|
||||
"billing.subscription_cancelled_description": undefined, // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
"billing.offering.free": undefined, // In English: "Free"
|
||||
"billing.offering.pro": undefined, // In English: "Professional"
|
||||
"billing.offering.business": undefined, // In English: "Business"
|
||||
"billing.cloud_storage": undefined, // In English: "Cloud Storage"
|
||||
"billing.ai_access": undefined, // In English: "AI Access"
|
||||
"billing.bandwidth": undefined, // In English: "Bandwidth"
|
||||
"billing.apps_and_games": undefined, // In English: "Apps & Games"
|
||||
"billing.upgrade_to_pro": undefined, // In English: "Upgrade to %strong%"
|
||||
"billing.switch_to": undefined, // In English: "Switch to %strong%"
|
||||
"billing.payment_setup": undefined, // In English: "Payment Setup"
|
||||
"billing.back": undefined, // In English: "Back"
|
||||
"billing.you_are_now_subscribed_to": undefined, // In English: "You are now subscribed to %strong% tier."
|
||||
"billing.you_are_now_subscribed_to_without_tier": undefined, // In English: "You are now subscribed"
|
||||
"billing.subscription_cancellation_confirmation": undefined, // In English: "Are you sure you want to cancel your subscription?"
|
||||
"billing.subscription_setup": undefined, // In English: "Subscription Setup"
|
||||
"billing.cancel_it": undefined, // In English: "Cancel It"
|
||||
"billing.keep_it": undefined, // In English: "Keep It"
|
||||
"billing.subscription_resumed": undefined, // In English: "Your %strong% subscription has been resumed!"
|
||||
"billing.upgrade_now": undefined, // In English: "Upgrade Now"
|
||||
"billing.upgrade": undefined, // In English: "Upgrade"
|
||||
"billing.currently_on_free_plan": undefined, // In English: "You are currently on the free plan."
|
||||
"billing.download_receipt": undefined, // In English: "Download Receipt"
|
||||
"billing.subscription_check_error": undefined, // In English: "A problem occurred while checking your subscription status."
|
||||
"billing.email_confirmation_needed": undefined, // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
"billing.sub_cancelled_but_valid_until": undefined, // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
"billing.current_plan_until_end_of_period": undefined, // In English: "Your current plan until the end of this billing period."
|
||||
"billing.current_plan": undefined, // In English: "Current plan"
|
||||
"billing.cancelled_subscription_tier": undefined, // In English: "Cancelled Subscription (%%)"
|
||||
"billing.manage": undefined, // In English: "Manage"
|
||||
"billing.limited": undefined, // In English: "Limited"
|
||||
"billing.expanded": undefined, // In English: "Expanded"
|
||||
"billing.accelerated": undefined, // In English: "Accelerated"
|
||||
"billing.enjoy_msg": undefined, // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
"billing.download_invoice": "청구서 다운로드", // added "invoice"
|
||||
"billing.payment_method": "결제 수단", // changed 방법 to 수단 which is more widely used in payment UIs
|
||||
"billing.payment_method_updated": "결제 수단이 변경되었습니다!", // changed to more natural Korean
|
||||
"billing.confirm_payment_method": "결제 수단 확인", // In English: "Confirm Payment Method"
|
||||
"billing.payment_history": "결제 내역", // In English: "Payment History"
|
||||
"billing.refunded": "환불 완료", // In English: "Refunded"
|
||||
"billing.paid": "결제 완료", // In English: "Paid"
|
||||
"billing.ok": "확인", // In English: "OK"
|
||||
"billing.resume_subscription": "구독 재개", // In English: "Resume Subscription"
|
||||
"billing.subscription_cancelled": "구독이 취소되었습니다.", // In English: "Your subscription has been canceled."
|
||||
"billing.subscription_cancelled_description":
|
||||
"청구 기간이 끝날 때까지 구독을 계속 이용할 수 있습니다.", // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
"billing.offering.free": "무료", // In English: "Free"
|
||||
"billing.offering.pro": "프로", // In English: "Professional"
|
||||
"billing.offering.business": "비즈니스", // In English: "Business"
|
||||
"billing.cloud_storage": "클라우드 저장소", // In English: "Cloud Storage"
|
||||
"billing.ai_access": "AI 접근", // In English: "AI Access"
|
||||
"billing.bandwidth": "대역폭", // In English: "Bandwidth"
|
||||
"billing.apps_and_games": "앱 및 게임", // In English: "Apps & Games"
|
||||
"billing.upgrade_to_pro": "%strong%으로 업그레이드", // In English: "Upgrade to %strong%" ; Important Translation note: 으 is omitted when it placed after a vowel, meaning: when putting free, pro and business in front of 으로 you need to change it to 로 only (example: "무료로" "프로로" "비즈니스로")
|
||||
"billing.switch_to": "%strong%으로 변경", // In English: "Switch to %strong%", Translation note: same logic from above regarding 으로 applies here too
|
||||
"billing.payment_setup": "결제 설정", // In English: "Payment Setup"
|
||||
"billing.back": "뒤로", // In English: "Back"
|
||||
"billing.you_are_now_subscribed_to":
|
||||
"%strong% 플랜으로 구독이 완료되었습니다.", // In English: "You are now subscribed to %strong% tier."
|
||||
"billing.you_are_now_subscribed_to_without_tier": "구독이 완료되었습니다", // In English: "You are now subscribed"
|
||||
"billing.subscription_cancellation_confirmation":
|
||||
"정말 구독을 취소하시겠습니까?", // In English: "Are you sure you want to cancel your subscription?"
|
||||
"billing.subscription_setup": "구독 설정", // In English: "Subscription Setup"
|
||||
"billing.cancel_it": "취소하기", // In English: "Cancel It"
|
||||
"billing.keep_it": "유지하기", // In English: "Keep It"
|
||||
"billing.subscription_resumed": "귀하의 %strong% 구독이 재개되었습니다!", // In English: "Your %strong% subscription has been resumed!"
|
||||
"billing.upgrade_now": "지금 업그레이드", // In English: "Upgrade Now"
|
||||
"billing.upgrade": "업그레이드", // In English: "Upgrade"
|
||||
"billing.currently_on_free_plan": "현재 무료 플랜을 이용 중입니다.", // In English: "You are currently on the free plan."
|
||||
"billing.download_receipt": "영수증 다운로드", // In English: "Download Receipt"
|
||||
"billing.subscription_check_error":
|
||||
"구독 상태를 확인하는 중 문제가 발생했습니다.", // In English: "A problem occurred while checking your subscription status."
|
||||
"billing.email_confirmation_needed":
|
||||
"이메일이 인증되지 않았습니다. 인증 코드를 보내드리겠습니다.", // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
"billing.sub_cancelled_but_valid_until":
|
||||
"구독이 취소되었으며, 청구 기간이 끝나면 자동으로 무료 플랜으로 전환됩니다. 구독을 다시 설정할 경우에만 비용이 부과됩니다.", // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
"billing.current_plan_until_end_of_period":
|
||||
"청구 기간이 끝날 때까지 유지되는 현재 플랜입니다.", // In English: "Your current plan until the end of this billing period."
|
||||
"billing.current_plan": "현재 플랜", // In English: "Current plan" ; depending on the context you could use: "구독 중인 플랜" (plan you are subscribed to)
|
||||
"billing.cancelled_subscription_tier": "취소된 구독 (%%)", // In English: "Cancelled Subscription (%%)"
|
||||
"billing.manage": "관리", // In English: "Manage"
|
||||
"billing.limited": "제한됨", // In English: "Limited"
|
||||
"billing.expanded": "확장됨", // In English: "Expanded"
|
||||
"billing.accelerated": "가속됨", // In English: "Accelerated"
|
||||
"billing.enjoy_msg": "클라우드 저장소 %% 등 다양한 혜택을 즐겨보세요", // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -393,55 +393,52 @@ const ku = {
|
||||
"You can't share with yourself.": "ناتوانیت لەگەڵ خودی خۆت بڵاوی کەیتەوە", // In English: "You can't share with yourself."
|
||||
"This user already has access to this item": "ئەم بەکارهێنەرە پێشتر ڕێپێدراوە بۆ ئەم فایلە", // In English: "This user already has access to this item"
|
||||
|
||||
// ----------------------------------------
|
||||
// Missing translations:
|
||||
// ----------------------------------------
|
||||
"billing.change_payment_method": "بیگۆڕە", // In English: "Change"
|
||||
"billing.cancel": "پاشگەزبوونەوە", // In English: "Cancel"
|
||||
"billing.download_invoice": "داگرتن", // In English: "Download"
|
||||
"billing.payment_method": "شێوازی پارەدان", // In English: "Payment Method"
|
||||
"billing.payment_method_updated": "شێوازی پارەدان نوێکراوە!", // In English: "Payment method updated!"
|
||||
"billing.confirm_payment_method": "پشتڕاستکردنەوەی شێوازی پارەدان", // In English: "Confirm Payment Method"
|
||||
"billing.payment_history": "مێژووی مامەڵەکان", // In English: "Payment History"
|
||||
"billing.refunded": "گەڕێندراوە", // In English: "Refunded"
|
||||
"billing.paid": "پارە درا", // In English: "Paid"
|
||||
"billing.ok": "باشە", // In English: "OK"
|
||||
"billing.resume_subscription": "بەشداریکردنەوە", // In English: "Resume Subscription"
|
||||
"billing.subscription_cancelled": "بەشداریکردنت هەڵوەشێندراوە.", // In English: "Your subscription has been canceled."
|
||||
"billing.subscription_cancelled_description": ".هێشتا بەشداریکردنت ماوە، تا کۆتایی ئەو ماوەیەی پارەت دابوو", // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
"billing.offering.free": "بەخۆڕایی", // In English: "Free"
|
||||
"billing.offering.pro": "پڕۆ", // In English: "Professional"
|
||||
"billing.offering.business": "بزنس", // In English: "Business"
|
||||
"billing.cloud_storage": "بیرگەی کڵاود", // In English: "Cloud Storage"
|
||||
"billing.ai_access": "دەستگەیشتن بە ژیریی دەستکرد", // In English: "AI Access"
|
||||
"billing.bandwidth": "باندویدث", // In English: "Bandwidth"
|
||||
"billing.apps_and_games": "بەرنامە و یاری", // In English: "Apps & Games"
|
||||
"billing.upgrade_to_pro": "بەرزکردنەوە بۆ %strong%", // In English: "Upgrade to %strong%"
|
||||
"billing.switch_to": "گۆڕین بۆ %strong%", // In English: "Switch to %strong%"
|
||||
"billing.payment_setup": "ڕێکخستنی پارەدان", // In English: "Payment Setup"
|
||||
"billing.back": "گەڕانەوە", // In English: "Back"
|
||||
"billing.you_are_now_subscribed_to": "تۆ ئێستا بەشداربووی لە ئاستی %strong%.", // In English: "You are now subscribed to %strong% tier."
|
||||
"billing.you_are_now_subscribed_to_without_tier": "تۆ ئێستا بەشداربووی", // In English: "You are now subscribed"
|
||||
"billing.subscription_cancellation_confirmation": "دڵنیایت دەتەوێت بەشداربوونت هەڵبوەشێنیتەوە؟", // In English: "Are you sure you want to cancel your subscription?"
|
||||
"billing.subscription_setup": "ڕێکخستنی بەشداریکردن", // In English: "Subscription Setup"
|
||||
"billing.cancel_it": "هەڵیبوەشێنەوە", // In English: "Cancel It"
|
||||
"billing.keep_it": "بیهێڵەوە", // In English: "Keep It"
|
||||
"billing.subscription_resumed": "بەشداریکردنت لە %strong% دەستیپێکردەوە!", // In English: "Your %strong% subscription has been resumed!"
|
||||
"billing.upgrade_now": "ئێستا بەرزی بکەوە", // In English: "Upgrade Now"
|
||||
"billing.upgrade": "بەرزکردنەوە", // In English: "Upgrade"
|
||||
"billing.currently_on_free_plan": "لە ئێستادا لەسەر پلانی بێ بەرامبەریت.", // In English: "You are currently on the free plan."
|
||||
"billing.download_receipt": "داگرتنی پسووڵە", // In English: "Download Receipt"
|
||||
"billing.subscription_check_error": "هەڵەیەک ڕوویدا لەکاتی پشکنینی دۆخی بەشداربوونت.", // In English: "A problem occurred while checking your subscription status."
|
||||
"billing.email_confirmation_needed": "ئیمێڵەکەت هێشتا نەسەلمێندراوە، کۆدێکت بۆ دەنێرین بۆ ئەوەی ئێستا بیسەلمێنیت.", // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
"billing.sub_cancelled_but_valid_until": "بەشداریکردنت هەڵوەشاندەوە، لە کۆتایی ئەو ماوەیەی پارەت داوە خودکارانە دەگوازرێیەوە بۆ ئاستی خۆڕایی، مەگەر دووبارە بەشداریبکەیەوە.", // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
"billing.current_plan_until_end_of_period": "پلانی ئێستات تا کۆتایی ئەو ماوەیەی پارەت داوە.", // In English: "Your current plan until the end of this billing period."
|
||||
"billing.current_plan": "پلانی ئێستا", // In English: "Current plan"
|
||||
"billing.cancelled_subscription_tier": "بەشداریکردنی (%%) هەڵوەشێندراوە", // In English: "Cancelled Subscription (%%)"
|
||||
"billing.manage": "بەڕێوەبردن", // In English: "Manage"
|
||||
"billing.limited": "سنووردار", // In English: "Limited"
|
||||
"billing.expanded": "کشاندراو", // In English: "Expanded"
|
||||
"billing.accelerated": "تاودراو", // In English: "Accelerated"
|
||||
"billing.enjoy_msg": "چێژوەرگرە لە بیرگەی کڵاود و چەندین خزمەتگوزاری تر.", // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
"billing.change_payment_method": "گۆڕانکاری",
|
||||
"billing.cancel": "بڕینەوە",
|
||||
"billing.download_invoice": "داونلۆد بکە",
|
||||
"billing.payment_method": "شێوازی پارەدان",
|
||||
"billing.payment_method_updated": "شێوازی پارەدان نوێ کراوەتەوە!",
|
||||
"billing.confirm_payment_method": "دروستکردنی شێوازی پارەدان",
|
||||
"billing.payment_history": "مێژووی پارەدان",
|
||||
"billing.refunded": "بەپێچەوانە کراوە",
|
||||
"billing.paid": "پارەی دا",
|
||||
"billing.ok": "باشە",
|
||||
"billing.resume_subscription": "پاشەکەوتی بەردەوام بکە",
|
||||
"billing.subscription_cancelled": "بەژداربوونەکەت هەڵوەشێنراوەتەوە",
|
||||
"billing.subscription_cancelled_description": "تا کۆتایی ئەم ماوەیە تۆ هێشتا دەستت بە بەشداربوونەکەت هەیە",
|
||||
"billing.offering.free": "بە خۆڕایی",
|
||||
"billing.offering.pro": "پیشەیی",
|
||||
"billing.offering.business": "بزنس",
|
||||
"billing.cloud_storage": "خزێنەی هەور",
|
||||
"billing.ai_access": "دەستڕاگەیشتن بە AI",
|
||||
"billing.bandwidth": "باندفیدت",
|
||||
"billing.apps_and_games": "ئەپەکان & یارییەکان",
|
||||
"billing.upgrade_to_pro": "بە %strong% بەرز بکەرەوە",
|
||||
"billing.switch_to": "گۆڕە بۆ %strong%",
|
||||
"billing.payment_setup": "بەکارھێنانی پارەدان",
|
||||
"billing.back": "باک",
|
||||
"billing.you_are_now_subscribed_to": "ئێستا تۆ بەشداریت لە %strong% tier",
|
||||
"billing.you_are_now_subscribed_to_without_tier": "ئێستا تۆ بەشداریت",
|
||||
"billing.subscription_cancellation_confirmation": "ئایا دڵنیایت کە دەتەوێت بەشداربوونەکەت هەڵوەشێنیتەوە؟",
|
||||
"billing.subscription_setup": "دەستکاریی بەشداربوون",
|
||||
"billing.cancel_it": "داوایی لێ بکەوە",
|
||||
"billing.keep_it": "هێشتەوە",
|
||||
"billing.subscription_resumed": "بەژداربوونت %strong% دەستپێکرایەوە!",
|
||||
"billing.upgrade_now": "ئێستا نوێکەرەوە",
|
||||
"billing.upgrade": "Upgrade",
|
||||
"billing.currently_on_free_plan": "ئێستا لە پلانی بێبەرامبەریت",
|
||||
"billing.download_receipt": "دانەوەی وەرگیراو",
|
||||
"billing.subscription_check_error": "کێشەیەک ڕوویدا لەکاتی پشکنینی دۆخی بەشداربوونەکەت",
|
||||
"billing.email_confirmation_needed": " ئیمەیڵەکەت پشتڕاست نەکراوەتەوە. کۆدێکت بۆ دەنێرین بۆ پشتڕاستکردنەوەی ئێستا",
|
||||
"billing.sub_cancelled_but_valid_until": "تۆ بەشداربوونەکەت هەڵوەشاندەوە و بە ئۆتۆماتیکی دەگۆڕێت بۆ پلەی خۆڕایی لە کۆتایی ماوەی فۆڕمی فۆرم. جارێكی دیكە هیچ پارەیەكتان لێناگیرێت مەگەر دووبارە بەشداربن",
|
||||
"billing.current_plan_until_end_of_period": "پلانی ئێستای تۆ تا کۆتایی ئەم ماوەیە بۆ فۆڕمی فۆرم",
|
||||
"billing.current_plan": "پلانی ئێستا",
|
||||
"billing.cancelled_subscription_tier": "بەژمارەی هەڵوەشێندراو (%%) ",
|
||||
"billing.manage": "بەڕێوەبەری",
|
||||
"billing.limited": "Limited",
|
||||
"billing.expanded": "بڵاوکراوەتەوە",
|
||||
"billing.accelerated": "بە خێرایی",
|
||||
"billing.enjoy_msg": "%% لە هەڵگرتنی هەور و سوودی تر وەربگرە"
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -364,56 +364,53 @@ const sv = {
|
||||
"This user already has access to this item":
|
||||
"Den här användaren har redan åtkomst till det här objektet", // In English: "This user already has access to this item"
|
||||
|
||||
// ----------------------------------------
|
||||
// Missing translations:
|
||||
// ----------------------------------------
|
||||
"plural_suffix": undefined, // In English: "s"
|
||||
"billing.change_payment_method": undefined, // In English: "Change"
|
||||
"billing.cancel": undefined, // In English: "Cancel"
|
||||
"billing.download_invoice": undefined, // In English: "Download"
|
||||
"billing.payment_method": undefined, // In English: "Payment Method"
|
||||
"billing.payment_method_updated": undefined, // In English: "Payment method updated!"
|
||||
"billing.confirm_payment_method": undefined, // In English: "Confirm Payment Method"
|
||||
"billing.payment_history": undefined, // In English: "Payment History"
|
||||
"billing.refunded": undefined, // In English: "Refunded"
|
||||
"billing.paid": undefined, // In English: "Paid"
|
||||
"billing.ok": undefined, // In English: "OK"
|
||||
"billing.resume_subscription": undefined, // In English: "Resume Subscription"
|
||||
"billing.subscription_cancelled": undefined, // In English: "Your subscription has been canceled."
|
||||
"billing.subscription_cancelled_description": undefined, // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
"billing.offering.free": undefined, // In English: "Free"
|
||||
"billing.offering.pro": undefined, // In English: "Professional"
|
||||
"billing.offering.business": undefined, // In English: "Business"
|
||||
"billing.cloud_storage": undefined, // In English: "Cloud Storage"
|
||||
"billing.ai_access": undefined, // In English: "AI Access"
|
||||
"billing.bandwidth": undefined, // In English: "Bandwidth"
|
||||
"billing.apps_and_games": undefined, // In English: "Apps & Games"
|
||||
"billing.upgrade_to_pro": undefined, // In English: "Upgrade to %strong%"
|
||||
"billing.switch_to": undefined, // In English: "Switch to %strong%"
|
||||
"billing.payment_setup": undefined, // In English: "Payment Setup"
|
||||
"billing.back": undefined, // In English: "Back"
|
||||
"billing.you_are_now_subscribed_to": undefined, // In English: "You are now subscribed to %strong% tier."
|
||||
"billing.you_are_now_subscribed_to_without_tier": undefined, // In English: "You are now subscribed"
|
||||
"billing.subscription_cancellation_confirmation": undefined, // In English: "Are you sure you want to cancel your subscription?"
|
||||
"billing.subscription_setup": undefined, // In English: "Subscription Setup"
|
||||
"billing.cancel_it": undefined, // In English: "Cancel It"
|
||||
"billing.keep_it": undefined, // In English: "Keep It"
|
||||
"billing.subscription_resumed": undefined, // In English: "Your %strong% subscription has been resumed!"
|
||||
"billing.upgrade_now": undefined, // In English: "Upgrade Now"
|
||||
"billing.upgrade": undefined, // In English: "Upgrade"
|
||||
"billing.currently_on_free_plan": undefined, // In English: "You are currently on the free plan."
|
||||
"billing.download_receipt": undefined, // In English: "Download Receipt"
|
||||
"billing.subscription_check_error": undefined, // In English: "A problem occurred while checking your subscription status."
|
||||
"billing.email_confirmation_needed": undefined, // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
"billing.sub_cancelled_but_valid_until": undefined, // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
"billing.current_plan_until_end_of_period": undefined, // In English: "Your current plan until the end of this billing period."
|
||||
"billing.current_plan": undefined, // In English: "Current plan"
|
||||
"billing.cancelled_subscription_tier": undefined, // In English: "Cancelled Subscription (%%)"
|
||||
"billing.manage": undefined, // In English: "Manage"
|
||||
"billing.limited": undefined, // In English: "Limited"
|
||||
"billing.expanded": undefined, // In English: "Expanded"
|
||||
"billing.accelerated": undefined, // In English: "Accelerated"
|
||||
"billing.enjoy_msg": undefined, // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
"plural_suffix": "", // In English: "s" (Plural suffix is context dependent in Swedish, it can be "or", "ar", "er", "en" or just no suffix)
|
||||
"billing.change_payment_method": "Ändra", // In English: "Change"
|
||||
"billing.cancel": "Avbryt", // In English: "Cancel"
|
||||
"billing.download_invoice": "Ladda ner", // In English: "Download"
|
||||
"billing.payment_method": "Betalningsmetod", // In English: "Payment Method"
|
||||
"billing.payment_method_updated": "Betalningsmetod uppdaterad!", // In English: "Payment method updated!"
|
||||
"billing.confirm_payment_method": "Bekräfta Betalningsmetod", // In English: "Confirm Payment Method"
|
||||
"billing.payment_history": "Betalningshistorik", // In English: "Payment History"
|
||||
"billing.refunded": "Återbetalas", // In English: "Refunded"
|
||||
"billing.paid": "Betalt", // In English: "Paid"
|
||||
"billing.ok": "OK", // In English: "OK"
|
||||
"billing.resume_subscription": "Återuppta Prenumeration", // In English: "Resume Subscription"
|
||||
"billing.subscription_cancelled": "Din prenumeration har avbrutits.", // In English: "Your subscription has been canceled."
|
||||
"billing.subscription_cancelled_description": "Du har fortfarande tillgång till din prenumeration fram till slutet av denna faktureringsperiod.", // In English: "You will still have access to your subscription until the end of this billing period."
|
||||
"billing.offering.free": "Gratis", // In English: "Free"
|
||||
"billing.offering.pro": "Professionell", // In English: "Professional"
|
||||
"billing.offering.business": "Företag", // In English: "Business"
|
||||
"billing.cloud_storage": "Molnlagring", // In English: "Cloud Storage"
|
||||
"billing.ai_access": "AI Tillgång", // In English: "AI Access"
|
||||
"billing.bandwidth": "Bandbredd", // In English: "Bandwidth"
|
||||
"billing.apps_and_games": "Appar & Spel", // In English: "Apps & Games"
|
||||
"billing.upgrade_to_pro": "Uppgradera till %strong%", // In English: "Upgrade to %strong%"
|
||||
"billing.switch_to": "Byt till %strong%", // In English: "Switch to %strong%"
|
||||
"billing.payment_setup": "Betalningsinställningar", // In English: "Payment Setup"
|
||||
"billing.back": "Tillbaka", // In English: "Back"
|
||||
"billing.you_are_now_subscribed_to": "Du prenumererar nu på %strong% tier.", // In English: "You are now subscribed to %strong% tier."
|
||||
"billing.you_are_now_subscribed_to_without_tier": "Du är nu prenumererad", // In English: "You are now subscribed"
|
||||
"billing.subscription_cancellation_confirmation": "Är du säker på att du vill avsluta din prenumeration?", // In English: "Are you sure you want to cancel your subscription?"
|
||||
"billing.subscription_setup": "Prenumerationsinställningar", // In English: "Subscription Setup"
|
||||
"billing.cancel_it": "Avbryt det", // In English: "Cancel It"
|
||||
"billing.keep_it": "Behåll det", // In English: "Keep It"
|
||||
"billing.subscription_resumed": "Din %strong% prenumeration har återupptagits!", // In English: "Your %strong% subscription has been resumed!"
|
||||
"billing.upgrade_now": "Uppgradera nu", // In English: "Upgrade Now"
|
||||
"billing.upgrade": "Uppgradera", // In English: "Upgrade"
|
||||
"billing.currently_on_free_plan": "Du har för närvarande den kostnadsfria planen.", // In English: "You are currently on the free plan."
|
||||
"billing.download_receipt": "Ladda ner Kvitto", // In English: "Download Receipt"
|
||||
"billing.subscription_check_error": "Ett problem uppstod när du kontrollerade din prenumerationsstatus.", // In English: "A problem occurred while checking your subscription status."
|
||||
"billing.email_confirmation_needed": "Din e-post har inte bekräftats. Vi skickar dig en kod för att bekräfta den nu.", // In English: "Your email has not been confirmed. We'll send you a code to confirm it now."
|
||||
"billing.sub_cancelled_but_valid_until": "Du har sagt upp din prenumeration och den byter automatiskt till gratisnivån i slutet av faktureringsperioden. Du kommer inte att debiteras igen om du inte prenumererar på nytt.", // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe."
|
||||
"billing.current_plan_until_end_of_period": "Din nuvarande plan fram till slutet av denna faktureringsperiod.", // In English: "Your current plan until the end of this billing period."
|
||||
"billing.current_plan": "Nuvarande plan", // In English: "Current plan"
|
||||
"billing.cancelled_subscription_tier": "Avbruten Prenumeration (%%)", // In English: "Cancelled Subscription (%%)"
|
||||
"billing.manage": "Hantera", // In English: "Manage"
|
||||
"billing.limited": "Begränsad", // In English: "Limited"
|
||||
"billing.expanded": "Utökad", // In English: "Expanded"
|
||||
"billing.accelerated": "Accelererad", // In English: "Accelerated"
|
||||
"billing.enjoy_msg": "Njut av %% av Cloud Storage plus andra förmåner.", // In English: "Enjoy %% of Cloud Storage plus other benefits."
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -72,18 +72,18 @@ export default {
|
||||
hu,
|
||||
hy,
|
||||
id,
|
||||
it,
|
||||
ig,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
ku,
|
||||
nb,
|
||||
nl,
|
||||
nn,
|
||||
ro,
|
||||
ru,
|
||||
pl,
|
||||
pt,
|
||||
ro,
|
||||
ru,
|
||||
sv,
|
||||
ta,
|
||||
th,
|
||||
|
Loading…
Reference in New Issue
Block a user