diff --git a/deploy/helm/fluxer/templates/NOTES.txt b/deploy/helm/fluxer/templates/NOTES.txt index e1ad0b6be..724467bbd 100644 --- a/deploy/helm/fluxer/templates/NOTES.txt +++ b/deploy/helm/fluxer/templates/NOTES.txt @@ -9,9 +9,13 @@ Open {{ $o }} once every pod is ready: The admin panel is at {{ $o }}{{ $v.adminBasePath }}. -Register the first account and finish the setup wizard in the same sitting. -Until the wizard is finished, any account that registers can change the -instance configuration. +The setup wizard asks for the setup token before it creates the admin account: + + kubectl -n {{ .Release.Namespace }} get secret {{ include "fluxer.secretName" . }} -o jsonpath='{.data.FLUXER_SETUP_TOKEN}' | base64 -d + +A Secret with no FLUXER_SETUP_TOKEN has a token the API wrote to its log once: + + kubectl -n {{ .Release.Namespace }} logs deployment/api | grep -i 'setup token' {{- if eq $v.edge.expose "loadBalancer" }} Point DNS for {{ $v.domain }} at the external address of the edge-public Service: diff --git a/deploy/helm/fluxer/templates/_helpers.tpl b/deploy/helm/fluxer/templates/_helpers.tpl index 55ec54f0c..267977434 100644 --- a/deploy/helm/fluxer/templates/_helpers.tpl +++ b/deploy/helm/fluxer/templates/_helpers.tpl @@ -185,6 +185,7 @@ public: {{ substr 56 121 $der | b64enc | replace "+" "-" | replace "/" "_" | tri {{- else if and (hasPrefix "FLUXER_LIVEKIT_" $k) (eq (include "fluxer.livekitMode" $) "off") }} {{- else if and (eq $k "FLUXER_VALKEY_PASSWORD") (not $.Values.valkey.enabled) }} {{- else if and (eq $k "FLUXER_NATS_AUTH_TOKEN") (not $.Values.nats.enabled) }} +{{- else if and (eq $k "FLUXER_SETUP_TOKEN") (or $.Values.secrets.existingSecret (and (not $.Values.secrets.generate) (not (include "fluxer.string" (get $.Values.secrets.values $k | default ""))))) }} {{- else }} {{- $keys = append $keys $k }} {{- end }} diff --git a/deploy/helm/fluxer/templates/_validate.tpl b/deploy/helm/fluxer/templates/_validate.tpl index 125fdccac..5e30259a1 100644 --- a/deploy/helm/fluxer/templates/_validate.tpl +++ b/deploy/helm/fluxer/templates/_validate.tpl @@ -149,7 +149,7 @@ {{- fail "FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must be standard base64 of at least 32 bytes, for example the output of openssl rand -base64 32" }} {{- end }} {{- end }} -{{- $long := list "FLUXER_SUDO_MODE_SECRET" "FLUXER_CONNECTION_INITIATION_SECRET" "FLUXER_PROFILE_PSEUDONYM_SECRET" "FLUXER_GATEWAY_RPC_AUTH_TOKEN" "FLUXER_ERLANG_COOKIE" "FLUXER_MEDIA_PROXY_SECRET_KEY" "FLUXER_ADMIN_SECRET_KEY_BASE" "FLUXER_ADMIN_OAUTH_CLIENT_SECRET" }} +{{- $long := list "FLUXER_SUDO_MODE_SECRET" "FLUXER_CONNECTION_INITIATION_SECRET" "FLUXER_PROFILE_PSEUDONYM_SECRET" "FLUXER_GATEWAY_RPC_AUTH_TOKEN" "FLUXER_ERLANG_COOKIE" "FLUXER_MEDIA_PROXY_SECRET_KEY" "FLUXER_ADMIN_SECRET_KEY_BASE" "FLUXER_ADMIN_OAUTH_CLIENT_SECRET" "FLUXER_SETUP_TOKEN" }} {{- if ne (include "fluxer.livekitMode" .) "off" }} {{- $long = append $long "FLUXER_LIVEKIT_API_SECRET" }} {{- end }} diff --git a/deploy/helm/fluxer/values.schema.json b/deploy/helm/fluxer/values.schema.json index b27e3832b..7f2c1bf45 100644 --- a/deploy/helm/fluxer/values.schema.json +++ b/deploy/helm/fluxer/values.schema.json @@ -397,6 +397,12 @@ "null" ] }, + "FLUXER_SETUP_TOKEN": { + "type": [ + "string", + "null" + ] + }, "FLUXER_LIVEKIT_API_KEY": { "type": [ "string", diff --git a/deploy/helm/fluxer/values.yaml b/deploy/helm/fluxer/values.yaml index edfc396a3..32cd7a5b0 100644 --- a/deploy/helm/fluxer/values.yaml +++ b/deploy/helm/fluxer/values.yaml @@ -102,6 +102,10 @@ secrets: FLUXER_ADMIN_SECRET_KEY_BASE: "" # Client secret of the built-in admin OAuth application. FLUXER_ADMIN_OAUTH_CLIENT_SECRET: "" + # Setup token the wizard asks for before the first admin account. With + # existingSecret, or with generate false and no value here, the API makes + # one on its first start and writes it to its log once. + FLUXER_SETUP_TOKEN: "" # LiveKit API key. FLUXER_LIVEKIT_API_KEY: fluxer # LiveKit API secret, at least 32 characters. diff --git a/deploy/self-hosting/.env.example b/deploy/self-hosting/.env.example index 0f145f58b..58a30d487 100644 --- a/deploy/self-hosting/.env.example +++ b/deploy/self-hosting/.env.example @@ -344,6 +344,8 @@ FLUXER_DISCOVERY_ENABLED=true #FLUXER_APP_STATUS_PAGE_URL= #FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL= #FLUXER_INSTANCE_SETUP_CONFIGURED=false +# Setup token the wizard asks for before the first admin account. 32 to 256 characters. Unset means the API generates one on the first start and writes it to its log once. +#FLUXER_SETUP_TOKEN= # How members sign in on a new instance, username or email. Unset means username. Read only on the first start. #FLUXER_ACCOUNT_IDENTITY= # Username tags on a new email instance. none gives unique names with no tag, random gives name#4821. Unset means none. A username instance always uses none. Read only on the first start. @@ -536,7 +538,7 @@ FLUXER_DISCOVERY_ENABLED=true # The bundled Valkey holds durable state as well as cache, so it runs with an # append-only file and with noeviction, which fails an over-limit write instead -# of dropping queued work. Change the policy only if that state lives elsewhere. +# of dropping queued work. Change the policy only if that state is kept elsewhere. #FLUXER_VALKEY_MAXMEMORY=192mb #FLUXER_VALKEY_MAXMEMORY_POLICY=noeviction #FLUXER_VALKEY_APPENDFSYNC=everysec diff --git a/deploy/self-hosting/docker-compose.yml b/deploy/self-hosting/docker-compose.yml index 7c2ca1e57..9c185311b 100644 --- a/deploy/self-hosting/docker-compose.yml +++ b/deploy/self-hosting/docker-compose.yml @@ -466,6 +466,7 @@ services: environment: <<: *fluxer-env FLUXER_API_PORT: "8080" + FLUXER_SETUP_TOKEN: ${FLUXER_SETUP_TOKEN:-} NODE_OPTIONS: --enable-source-maps${FLUXER_API_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_API_NODE_HEAP_MB}${FLUXER_API_NODE_OPTIONS:+ $FLUXER_API_NODE_OPTIONS} NODE_EXTRA_CA_CERTS: ${FLUXER_NODE_EXTRA_CA_CERTS:-/etc/ssl/certs/ca-certificates.crt} FLUXER_POSTGRES_MAX_CONNECTIONS: "${FLUXER_API_POSTGRES_MAX_CONNECTIONS:-25}" diff --git a/fluxer_api/src/api/Config.test.ts b/fluxer_api/src/api/Config.test.ts index 0dd14133f..d7b768f79 100644 --- a/fluxer_api/src/api/Config.test.ts +++ b/fluxer_api/src/api/Config.test.ts @@ -259,3 +259,42 @@ describe('buildAPIConfigFromMaster trusted callers', () => { ); }); }); + +async function setupTokenFromEnv(env: Record): Promise { + for (const [key, value] of Object.entries(env)) { + vi.stubEnv(key, value); + } + resetConfig(); + return buildAPIConfigFromMaster(await loadConfig()).instance.setup.token; +} + +describe('buildAPIConfigFromMaster setup token', () => { + const setupToken = 's'.repeat(32); + + test('reads the token from FLUXER_SETUP_TOKEN', async () => { + expect(await setupTokenFromEnv({FLUXER_SETUP_TOKEN: ` ${setupToken}\n`})).toBe(setupToken); + }); + + test.each(['', ' '])('leaves the token unset for %j', async (blank) => { + expect(await setupTokenFromEnv({FLUXER_SETUP_TOKEN: blank})).toBeNull(); + }); + + test('leaves the token unset when the variable is missing', async () => { + expect(await setupTokenFromEnv({})).toBeNull(); + }); + + test('accepts a token of 256 characters', async () => { + expect(await setupTokenFromEnv({FLUXER_SETUP_TOKEN: 's'.repeat(256)})).toBe('s'.repeat(256)); + }); + + test.each([ + ['shorter than 32 characters', 's'.repeat(31)], + ['longer than 256 characters', 's'.repeat(257)], + ['holding a form feed', `${'s'.repeat(16)}\f${'s'.repeat(16)}`], + ['holding a text direction override', `${'s'.repeat(16)}\u202E${'s'.repeat(16)}`], + ])('fails at boot on a token %s', async (_case, token) => { + await expect(setupTokenFromEnv({FLUXER_SETUP_TOKEN: token})).rejects.toThrow( + 'FLUXER_SETUP_TOKEN must be 32 to 256 printable characters', + ); + }); +}); diff --git a/fluxer_api/src/api/Config.ts b/fluxer_api/src/api/Config.ts index a9d222133..3eef9596b 100644 --- a/fluxer_api/src/api/Config.ts +++ b/fluxer_api/src/api/Config.ts @@ -8,6 +8,7 @@ import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import type {MasterConfig} from '@fluxer/config/src/MasterConfig'; import {smtpTlsModeFromSecure} from '@fluxer/constants/src/SmtpConstants'; import {parseIpAddress} from '@fluxer/ip_utils/src/IpAddress'; +import {normalizeString} from '@fluxer/schema/src/primitives/SchemaPrimitives'; import {parseGeoipSourceConfig, resolveGeoipRuntimeSourceConfig} from '@pkgs/geoip/src/GeoipStartup'; function extractHostname(url: string): string { @@ -123,6 +124,24 @@ function buildTrustedCallers(master: MasterConfig): Array { return trustedCallers; } +const SETUP_TOKEN_MIN_LENGTH = 32; +const SETUP_TOKEN_MAX_LENGTH = 256; + +function resolveSetupToken(master: MasterConfig): string | null { + const token = master.instance.setup.token?.trim(); + if (!token) return null; + if ( + token.length < SETUP_TOKEN_MIN_LENGTH || + token.length > SETUP_TOKEN_MAX_LENGTH || + normalizeString(token) !== token + ) { + throw new Error( + `FLUXER_SETUP_TOKEN must be ${SETUP_TOKEN_MIN_LENGTH} to ${SETUP_TOKEN_MAX_LENGTH} printable characters`, + ); + } + return token; +} + export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig { if (!master.internal) { throw new Error('internal configuration is required for the API'); @@ -402,6 +421,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig { }, setup: { configured: master.instance.setup.configured, + token: resolveSetupToken(master), }, accountIdentity: master.instance.account_identity, tagStyle: master.instance.tag_style, diff --git a/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts b/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts index 0140ef755..d457ff0ba 100644 --- a/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts +++ b/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts @@ -11,6 +11,7 @@ import { REGISTRATION_PENDING_APPROVAL_TRAIT, REGISTRATION_REJECTED_TRAIT, } from '@app/api/instance/InstanceConfigRepository'; +import {clearUsedSetupToken} from '@app/api/instance/SetupToken'; import {deriveSsoRedirectUri, normalizeAndValidateSsoConfig} from '@app/api/instance/SsoConfigValidation'; import {requireAdminACL} from '@app/api/middleware/AdminMiddleware'; import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware'; @@ -44,7 +45,6 @@ import {ExperimentDeliveryConfigSchema} from '@fluxer/schema/src/domains/experim import type {InstanceBranding} from '@fluxer/schema/src/domains/instance/InstanceSchemas'; import {SmtpEmailProvider} from '@pkgs/email/src/SmtpEmailProvider'; import type {Context} from 'hono'; -import {createMiddleware} from 'hono/factory'; const INSTANCE_BRANDING_ENTITY_ID = 0n; const ATTACHMENT_EXPIRY_RULE_FIELDS = [ @@ -187,49 +187,6 @@ function buildAdminIssuedRegistrationUrl(code: string): string { return `${baseUrl}/register?registration_url=${encodeURIComponent(code)}`; } -function requireSetupSessionOrAdminACL(requiredACL: string) { - const requireAcl = requireAdminACL(requiredACL); - return createMiddleware(async (ctx, next) => { - const user = ctx.get('user'); - const tokenType = ctx.get('authTokenType'); - if (user && tokenType === 'session') { - const appPublic = await getInstanceConfigRepository().getAppPublicConfig(); - if (!appPublic.setup.configured) { - ctx.set('adminUserId', user.id); - ctx.set('adminUserAcls', user.acls); - await next(); - return; - } - } - return requireAcl(ctx, next); - }); -} - -function hasAdminAuthenticationACL(acls: ReadonlySet): boolean { - return acls.has(AdminACLs.AUTHENTICATE) || acls.has(AdminACLs.WILDCARD); -} - -function completesInitialSetup(data: InstanceConfigUpdateRequest, setupConfigured: boolean): boolean { - return ( - !setupConfigured && - data.app_public?.setup != null && - readOptionalField(data.app_public.setup, 'configured') === true - ); -} - -async function grantSetupCompleterAdminACL(ctx: Context): Promise { - const user = ctx.get('user'); - if (!user || ctx.get('authTokenType') !== 'session' || hasAdminAuthenticationACL(user.acls)) { - return false; - } - const nextACLs = new Set(user.acls); - nextACLs.add(AdminACLs.WILDCARD); - const updatedUser = await ctx.get('userRepository').patchUpsert(user.id, {acls: nextACLs}, user.toRow()); - ctx.set('user', updatedUser); - ctx.set('adminUserAcls', updatedUser.acls); - return true; -} - function relayConsentStamp( current: PushRelayConfig, patch: PushRelayConfigUpdateRequest, @@ -346,7 +303,7 @@ export function InstanceConfigAdminController(app: HonoApp) { app.get( '/admin/instance/config', RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP), - requireSetupSessionOrAdminACL(AdminACLs.INSTANCE_CONFIG_VIEW), + requireAdminACL(AdminACLs.INSTANCE_CONFIG_VIEW), OpenAPI({ operationId: 'get_admin_instance_config', summary: 'Get instance configuration', @@ -374,7 +331,7 @@ export function InstanceConfigAdminController(app: HonoApp) { app.patch( '/admin/instance/config', RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY), - requireSetupSessionOrAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE), + requireAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE), Validator('json', InstanceConfigUpdateRequest), OpenAPI({ operationId: 'update_admin_instance_config', @@ -390,11 +347,6 @@ export function InstanceConfigAdminController(app: HonoApp) { const data = ctx.req.valid('json'); assertSelfHostedBillingSections(data); await assertBillingCompatibleWithPremiumMode(data); - const appPublicBeforeUpdate = completesInitialSetup(data, false) - ? await instanceConfigRepository.getAppPublicConfig() - : null; - const shouldGrantSetupCompleterAdmin = - appPublicBeforeUpdate !== null && completesInitialSetup(data, appPublicBeforeUpdate.setup.configured); if (data.gateway_rollout) { const patch = data.gateway_rollout; const landed = await instanceConfigRepository.updateGatewayRolloutConfig((current) => @@ -628,16 +580,14 @@ export function InstanceConfigAdminController(app: HonoApp) { ); } if (data.app_public?.setup) { + const configured = readOptionalField(data.app_public.setup, 'configured'); await instanceConfigRepository.setAppPublicConfig({ - setup: omitUndefinedFields({ - configured: readOptionalField(data.app_public.setup, 'configured'), - }), + setup: omitUndefinedFields({configured}), }); - } - let grantedSetupCompleterAdmin = false; - if (shouldGrantSetupCompleterAdmin) { - grantedSetupCompleterAdmin = await grantSetupCompleterAdminACL(ctx); - await instanceConfigRepository.markAdminBootstrapped(); + if (configured === true) { + await instanceConfigRepository.claimAdminBootstrap(); + await clearUsedSetupToken(instanceConfigRepository); + } } await recordAdminWrite(ctx, { targetType: 'instance_config', @@ -645,7 +595,6 @@ export function InstanceConfigAdminController(app: HonoApp) { action: 'update_instance_config', metadata: { sections: listSuppliedSections(data), - granted_acls: grantedSetupCompleterAdmin ? AdminACLs.WILDCARD : undefined, }, }); return ctx.json(await buildInstanceConfigResponse()); @@ -654,7 +603,7 @@ export function InstanceConfigAdminController(app: HonoApp) { app.post( '/admin/instance/config/branding-assets', RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY), - requireSetupSessionOrAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE), + requireAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE), Validator('json', BrandingAssetUploadRequest), OpenAPI({ operationId: 'create_admin_instance_branding_asset', @@ -690,7 +639,7 @@ export function InstanceConfigAdminController(app: HonoApp) { app.post( '/admin/instance/config/smtp-tests', RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY), - requireSetupSessionOrAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE), + requireAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE), Validator('json', InstanceEmailSmtpTestRequest), OpenAPI({ operationId: 'create_admin_instance_smtp_test', diff --git a/fluxer_api/src/api/admin/tests/audit_coverage/InstanceConfigAdminAuditCases.ts b/fluxer_api/src/api/admin/tests/audit_coverage/InstanceConfigAdminAuditCases.ts index f5035b859..bbb4bda4a 100644 --- a/fluxer_api/src/api/admin/tests/audit_coverage/InstanceConfigAdminAuditCases.ts +++ b/fluxer_api/src/api/admin/tests/audit_coverage/InstanceConfigAdminAuditCases.ts @@ -27,7 +27,6 @@ export const InstanceConfigAdminAuditCases: ReadonlyArray; @@ -94,12 +97,14 @@ interface ApplyPasswordResetParams { password: string; request: Request; afterPasswordSet?: () => Promise; + onPasswordWritten?: () => void; } interface CommitPasswordResetParams { user: User; passwordHash: string; webauthnIsSecondFactor: boolean; + onPasswordWritten?: () => void; } type ResetPasswordResult = @@ -242,6 +247,22 @@ export async function validateResetToken(ctx: ApiContext, token: string): Promis return true; } +function passwordResetClaimKey(token: string): string { + return `${PASSWORD_RESET_CLAIM_KEY_PREFIX}${crypto.createHash('sha256').update(token).digest('hex')}`; +} + +function claimPasswordResetToken(token: string): Promise { + return getKVClient().setnx(passwordResetClaimKey(token), '1', PASSWORD_RESET_TOKEN_TTL_SECONDS); +} + +async function releasePasswordResetTokenClaim(token: string): Promise { + try { + await getKVClient().del(passwordResetClaimKey(token)); + } catch (error) { + Logger.error({error}, 'Could not release the claim of a password reset token whose reset failed'); + } +} + export async function resetPassword( ctx: ApiContext, {data, request}: ResetPasswordParams, @@ -263,28 +284,42 @@ export async function resetPassword( if (await isPasswordPwned(ctx, data.password)) { throw InputValidationError.fromCode('password', ValidationErrorCodes.PASSWORD_IS_TOO_COMMON); } - if (tokenData.email !== null) { + if (!(await claimPasswordResetToken(data.token))) { + throw InputValidationError.fromCode('token', ValidationErrorCodes.INVALID_OR_EXPIRED_RESET_TOKEN); + } + let passwordWritten = false; + const onPasswordWritten = () => { + passwordWritten = true; + }; + try { + if (tokenData.email !== null) { + return await applyPasswordReset(ctx, { + user, + password: data.password, + request, + afterPasswordSet: () => users.deleteAllPasswordResetTokens(user.id), + onPasswordWritten, + }); + } return await applyPasswordReset(ctx, { - user, + user: currentUser, password: data.password, request, - afterPasswordSet: () => users.deleteAllPasswordResetTokens(user.id), + afterPasswordSet: async () => { + await users.deleteAllPasswordResetTokens(currentUser.id); + await new RecoveryKitRepository().delete(currentUser.id); + }, + onPasswordWritten, }); + } catch (error) { + if (!passwordWritten) await releasePasswordResetTokenClaim(data.token); + throw error; } - return await applyPasswordReset(ctx, { - user: currentUser, - password: data.password, - request, - afterPasswordSet: async () => { - await users.deleteAllPasswordResetTokens(currentUser.id); - await new RecoveryKitRepository().delete(currentUser.id); - }, - }); } export async function commitPasswordReset( ctx: ApiContext, - {user, passwordHash, webauthnIsSecondFactor}: CommitPasswordResetParams, + {user, passwordHash, webauthnIsSecondFactor, onPasswordWritten}: CommitPasswordResetParams, ): Promise { const {users} = ctx.services; const updates: Partial = { @@ -297,6 +332,7 @@ export async function commitPasswordReset( updates.authenticator_types = authenticatorTypes; } const updatedUser = await users.patchUpsert(user.id, updates, user.toRow()); + onPasswordWritten?.(); if (updates.authenticator_types) { await ctx.services.botMfaMirror.syncAuthenticatorTypesForOwner(updatedUser); } @@ -306,12 +342,12 @@ export async function commitPasswordReset( export async function applyPasswordReset( ctx: ApiContext, - {user, password, request, afterPasswordSet}: ApplyPasswordResetParams, + {user, password, request, afterPasswordSet, onPasswordWritten}: ApplyPasswordResetParams, ): Promise { const webauthnIsSecondFactor = await resolveWebAuthnSecondFactor(ctx, user); const hasMfa = user.authenticatorTypes.has(UserAuthenticatorTypes.TOTP) || webauthnIsSecondFactor; const passwordHash = await hashPassword(password); - const updatedUser = await commitPasswordReset(ctx, {user, passwordHash, webauthnIsSecondFactor}); + const updatedUser = await commitPasswordReset(ctx, {user, passwordHash, webauthnIsSecondFactor, onPasswordWritten}); await afterPasswordSet?.(); if (hasMfa) { return await createMfaTicketResponse(ctx, updatedUser, webauthnIsSecondFactor); diff --git a/fluxer_api/src/api/auth/AuthRegistration.ts b/fluxer_api/src/api/auth/AuthRegistration.ts index d24d4b8ac..5250cd375 100644 --- a/fluxer_api/src/api/auth/AuthRegistration.ts +++ b/fluxer_api/src/api/auth/AuthRegistration.ts @@ -25,6 +25,7 @@ import { REGISTRATION_PENDING_APPROVAL_TRAIT, type RegistrationUrlClaim, } from '@app/api/instance/InstanceConfigRepository'; +import {assertSetupToken, clearUsedSetupToken, isSetupTokenEnforced} from '@app/api/instance/SetupToken'; import type {SingleCommunityService} from '@app/api/instance/SingleCommunityService'; import type {InviteService} from '@app/api/invite/InviteService'; import {Logger} from '@app/api/Logger'; @@ -85,6 +86,11 @@ interface RegisterParams { requestCache: RequestCache; } +interface RegisterAccountParams extends RegisterParams { + clientIp: string; + setupTokenVerified: boolean; +} + export interface RegistrationDependencies { inviteService: InviteService; instanceConfigRepository: InstanceConfigRepository; @@ -114,16 +120,26 @@ export async function register( deps: RegistrationDependencies, params: RegisterParams, ): Promise { - if (!ctx.services.config.instance.selfHosted || (await deps.instanceConfigRepository.isAccountIdentityLocked())) { - return await registerAccount(ctx, deps, params); + const {config, rateLimit, cache} = ctx.services; + const clientIp = requireConfiguredClientIp(params.request, config.proxy); + const setupTokenVerified = await assertSetupToken({ + rateLimit, + config, + repository: deps.instanceConfigRepository, + presented: params.data.setup_token, + clientIp, + }); + const account = {...params, clientIp, setupTokenVerified}; + if (!config.instance.selfHosted || (await deps.instanceConfigRepository.isAccountIdentityLocked())) { + return await registerAccount(ctx, deps, account); } - return await withAccountIdentitySetupLock(ctx.services.cache, () => registerAccount(ctx, deps, params)); + return await withAccountIdentitySetupLock(cache, () => registerAccount(ctx, deps, account)); } async function registerAccount( ctx: ApiContext, deps: RegistrationDependencies, - {data, request, requestCache}: RegisterParams, + {data, request, requestCache, clientIp, setupTokenVerified}: RegisterAccountParams, ): Promise { const {users, snowflake, emailDnsValidation, config} = ctx.services; const {inviteService, instanceConfigRepository, singleCommunityService, discriminatorService, kvActivityTracker} = @@ -144,8 +160,9 @@ async function registerAccount( throw InputValidationError.fromCode('consent', ValidationErrorCodes.MUST_AGREE_TO_TOS_AND_PRIVACY_POLICY); } const now = new Date(); - const registrationAccess = await resolveRegistrationAccess(instanceConfigRepository, data.registration_url_code); - const clientIp = requireConfiguredClientIp(request, config.proxy); + const registrationAccess = setupTokenVerified + ? {pendingApproval: false, registrationUrl: null} + : await resolveRegistrationAccess(instanceConfigRepository, data.registration_url_code); const geoipResult = await lookupGeoip(clientIp); const countryCode = geoipResult.countryCode; const collectDateOfBirth = appPublicConfig.registration.collect_date_of_birth; @@ -168,7 +185,7 @@ async function registerAccount( } const rawEmail = usernameMode ? null : (data.email ?? null); const emailKey = rawEmail ? rawEmail.toLowerCase() : null; - const enforceRateLimits = !config.dev.relaxRegistrationRateLimits; + const enforceRateLimits = !config.dev.relaxRegistrationRateLimits && !setupTokenVerified; await enforceRegistrationRateLimits(ctx, {enforceRateLimits, clientIp, emailKey}); if (rawEmail) { if (isBlockedEmailDomain(extractEmailDomain(rawEmail))) { @@ -217,13 +234,14 @@ async function registerAccount( discriminator = await allocateDiscriminator(discriminatorService, usernameCandidate); } const username = usernameCandidate!; - const grantBootstrapAdmin = + const bootstrapAdminEligible = shouldAttemptBootstrapAdminGrant(config, { rawEmail, hasPassword: Boolean(data.password), usernameMode, pendingApproval: registrationAccess.pendingApproval, setupConfigured: appPublicConfig.setup.configured, + setupTokenVerified, }) && !(await instanceConfigRepository.isAdminBootstrapped()); if ( profileSubstringBlocklistCache.containsBannedSubstring('username', username) || @@ -258,7 +276,7 @@ async function registerAccount( privacy_agreed_at: requiresPrivacyConsent ? now : null, last_active_at: now, last_active_ip: clientIp, - acls: grantBootstrapAdmin ? new Set([AdminACLs.WILDCARD]) : null, + acls: null, traits: registrationAccess.pendingApproval ? new Set([REGISTRATION_PENDING_APPROVAL_TRAIT]) : null, version: 1, }; @@ -269,6 +287,7 @@ async function registerAccount( ); let user: User; let createAttempted = false; + let grantBootstrapAdmin = false; let usernameReservation: UsernameReservation | null = null; try { if (uniqueUsernames) { @@ -286,8 +305,9 @@ async function registerAccount( client_ip: clientIp, }); } + grantBootstrapAdmin = bootstrapAdminEligible && (await instanceConfigRepository.claimAdminBootstrap(userId)); createAttempted = true; - user = await users.create(userRow); + user = await users.create(grantBootstrapAdmin ? {...userRow, acls: new Set([AdminACLs.WILDCARD])} : userRow); } catch (error) { if (!createAttempted) { await withdrawSignupOfUncreatedAccount(instanceConfigRepository, { @@ -295,11 +315,16 @@ async function registerAccount( registrationUrlUse, pendingApproval: registrationAccess.pendingApproval, }); + } else if (grantBootstrapAdmin) { + await releaseBootstrapAdminOfUncreatedAccount(instanceConfigRepository, userId); } throw error; } finally { await usernameReservation?.release(); } + if (grantBootstrapAdmin) { + await clearUsedSetupToken(instanceConfigRepository); + } await users.upsertSettings( UserSettings.getDefaultUserSettings({ userId, @@ -348,9 +373,6 @@ async function registerAccount( user, origin: AuthSession.resolveSessionOrigin(ctx, request), }); - if (grantBootstrapAdmin) { - await instanceConfigRepository.markAdminBootstrapped(); - } return { user_id: user.id.toString(), token, @@ -365,11 +387,13 @@ function shouldAttemptBootstrapAdminGrant( usernameMode: boolean; pendingApproval: boolean; setupConfigured: boolean; + setupTokenVerified: boolean; }, ): boolean { + const claimedAccount = params.usernameMode ? params.hasPassword : params.rawEmail !== null; + if (isSetupTokenEnforced(config)) return params.setupTokenVerified && claimedAccount; const localDevInstance = config.nodeEnv === 'development' && !config.dev.testModeEnabled; const setupBootstrapOpen = !params.setupConfigured; - const claimedAccount = params.usernameMode ? params.hasPassword : params.rawEmail !== null; return ( (config.instance.selfHosted || localDevInstance || setupBootstrapOpen) && claimedAccount && !params.pendingApproval ); @@ -432,6 +456,20 @@ async function withdrawSignupOfUncreatedAccount( } } +async function releaseBootstrapAdminOfUncreatedAccount( + instanceConfigRepository: InstanceConfigRepository, + userId: UserID, +): Promise { + try { + await instanceConfigRepository.releaseAdminBootstrap(); + } catch (error) { + Logger.error( + {userId: userId.toString(), error}, + '[AuthRegistration] Failed to release the admin bootstrap claim of an account that was never created', + ); + } +} + async function resolveRegistrationAccess( instanceConfigRepository: InstanceConfigRepository, registrationUrlCode: string | null | undefined, diff --git a/fluxer_api/src/api/auth/tests/PasswordReset.test.ts b/fluxer_api/src/api/auth/tests/PasswordReset.test.ts index 81f46a094..9ee5f287b 100644 --- a/fluxer_api/src/api/auth/tests/PasswordReset.test.ts +++ b/fluxer_api/src/api/auth/tests/PasswordReset.test.ts @@ -12,7 +12,8 @@ import { import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {generateUniquePassword, HTTP_STATUS} from '@app/api/test/TestConstants'; import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; -import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; +import {UserRepository} from '@app/api/user/repositories/UserRepository'; +import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'; describe('Password reset flow', () => { let harness: ApiTestHarness; @@ -104,6 +105,61 @@ describe('Password reset flow', () => { const login = await loginUser(harness, {email: account.email, password: newPassword}); expect('token' in login && login.token.length > 0).toBe(true); }); + it('accepts a reset token for only one of several concurrent resets', async () => { + const account = await createTestAccount(harness); + await createBuilderWithoutAuth(harness) + .post('/auth/forgot') + .body({email: account.email}) + .expect(HTTP_STATUS.NO_CONTENT) + .execute(); + const emails = await listTestEmails(harness, {recipient: account.email}); + const token = findLastTestEmail(emails, 'password_reset')!.metadata!.token!; + const results = await Promise.all( + Array.from({length: 4}, () => + createBuilderWithoutAuth(harness) + .post('/auth/reset') + .body({token, password: generateUniquePassword()}) + .executeRaw(), + ), + ); + const statuses = results.map(({response}) => response.status).sort(); + expect(statuses).toEqual( + [HTTP_STATUS.OK, HTTP_STATUS.BAD_REQUEST, HTTP_STATUS.BAD_REQUEST, HTTP_STATUS.BAD_REQUEST].sort(), + ); + }); + it('keeps a reset token usable when the reset fails before the password is written', async () => { + const account = await createTestAccount(harness); + await createBuilderWithoutAuth(harness) + .post('/auth/forgot') + .body({email: account.email}) + .expect(HTTP_STATUS.NO_CONTENT) + .execute(); + const emails = await listTestEmails(harness, {recipient: account.email}); + const token = findLastTestEmail(emails, 'password_reset')!.metadata!.token!; + const patchUpsert = vi + .spyOn(UserRepository.prototype, 'patchUpsert') + .mockRejectedValueOnce(new Error('the user row write failed')); + try { + await createBuilderWithoutAuth(harness) + .post('/auth/reset') + .body({token, password: generateUniquePassword()}) + .expect(HTTP_STATUS.INTERNAL_SERVER_ERROR) + .execute(); + } finally { + patchUpsert.mockRestore(); + } + const newPassword = generateUniquePassword(); + const resetResp = await createBuilderWithoutAuth(harness) + .post('/auth/reset') + .body({token, password: newPassword}) + .execute(); + expect(resetResp.token.length).toBeGreaterThan(0); + await createBuilderWithoutAuth(harness) + .post('/auth/reset') + .body({token, password: generateUniquePassword()}) + .expect(HTTP_STATUS.BAD_REQUEST) + .execute(); + }); it('rejects invalid reset token', async () => { await createTestAccount(harness); await createBuilderWithoutAuth(harness) diff --git a/fluxer_api/src/api/auth/tests/Registration.test.ts b/fluxer_api/src/api/auth/tests/Registration.test.ts index 432c5f166..6b2068cc2 100644 --- a/fluxer_api/src/api/auth/tests/Registration.test.ts +++ b/fluxer_api/src/api/auth/tests/Registration.test.ts @@ -14,11 +14,19 @@ import { import {createUserID} from '@app/api/BrandedTypes'; import {getConfig} from '@app/api/Config'; import {applySharedListUpdate, resetSharedListsForTests} from '@app/api/infrastructure/activity/SharedLists'; -import {getInstanceConfigRepository, getUserRepository} from '@app/api/middleware/ServiceSingletons'; +import { + getInstanceConfigRepository, + getRateLimitService, + getUserRepository, +} from '@app/api/middleware/ServiceSingletons'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; +import {HTTP_STATUS} from '@app/api/test/TestConstants'; import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; -import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; +import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; +import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress'; +import {ms} from 'itty-time'; +import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'; function bootstrapRegistrationBody(prefix: string): Record { return { @@ -57,6 +65,37 @@ async function withBootstrapAdminConfig( } } +const SETUP_TOKEN = 'setup-token-for-tests-0123456789-abcdefghijklmnopqrstuvwxyz'; +const WRONG_SETUP_TOKEN = 'wrong-setup-token-0123456789-abcdefghijklmnopqrstuvwxyz'; +const SETUP_TOKEN_HASH_KEY = 'setup_token_hash'; +const TOO_MANY_REQUESTS = 429; + +interface SetupTokenRegistrationResponse { + user_id?: string; + token?: string; + registration_pending_approval?: boolean; + code?: string; + errors?: Array<{path: string; code: string}>; +} + +async function withSetupTokenInstance(callback: () => Promise): Promise { + const config = getConfig(); + const originalSelfHosted = config.instance.selfHosted; + const originalSetupToken = config.instance.setup.token; + const originalSetupConfigured = config.instance.setup.configured; + const originalRelaxRegistrationRateLimits = config.dev.relaxRegistrationRateLimits; + try { + config.instance.selfHosted = true; + config.instance.setup.token = SETUP_TOKEN; + await callback(); + } finally { + config.instance.selfHosted = originalSelfHosted; + config.instance.setup.token = originalSetupToken; + config.instance.setup.configured = originalSetupConfigured; + config.dev.relaxRegistrationRateLimits = originalRelaxRegistrationRateLimits; + } +} + async function expectUserACLs(userId: string, expectedACLs: Array): Promise { const user = await getUserRepository().findUniqueAssert(createUserID(BigInt(userId))); expect([...user.acls].sort()).toEqual([...expectedACLs].sort()); @@ -123,31 +162,45 @@ describe('Auth registration', () => { await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true); }); }); - it('allows setup-open sessions to fetch instance config when bootstrap marker is stale', async () => { + it('lets exactly one concurrent caller claim the admin bootstrap', async () => { + const instanceConfigRepository = getInstanceConfigRepository(); + const claims = await Promise.all(Array.from({length: 5}, () => instanceConfigRepository.claimAdminBootstrap())); + expect(claims.filter(Boolean)).toHaveLength(1); + await expect(instanceConfigRepository.isAdminBootstrapped()).resolves.toBe(true); + await expect(instanceConfigRepository.claimAdminBootstrap()).resolves.toBe(false); + }); + it('keeps instance config closed to sessions without admin ACLs while setup is open', async () => { await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => { const instanceConfigRepository = getInstanceConfigRepository(); await instanceConfigRepository.setAppPublicConfig({setup: {configured: false}}); - await instanceConfigRepository.markAdminBootstrapped(); - const account = await registerUser(harness, bootstrapRegistrationBody('stalesetupmarker')); + await instanceConfigRepository.claimAdminBootstrap(); + const account = await registerUser(harness, bootstrapRegistrationBody('setupopennonadmin')); await expectUserACLs(account.user_id, []); - await createBuilder(harness, account.token).get('/admin/instance/config').execute(); + await createBuilder(harness, account.token).get('/admin/instance/config').expect(HTTP_STATUS.FORBIDDEN).execute(); + await createBuilder(harness, account.token) + .patch('/admin/instance/config') + .body({app_public: {setup: {configured: true}}}) + .expect(HTTP_STATUS.FORBIDDEN) + .execute(); + await expectUserACLs(account.user_id, []); + await expect(instanceConfigRepository.getAppPublicConfig()).resolves.toMatchObject({ + setup: {configured: false}, + }); }); }); - it('repairs setup completer admin ACL when bootstrap marker is stale', async () => { + it('lets the bootstrap admin complete setup', async () => { await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => { const instanceConfigRepository = getInstanceConfigRepository(); await instanceConfigRepository.setAppPublicConfig({setup: {configured: false}}); - await instanceConfigRepository.markAdminBootstrapped(); - const account = await registerUser(harness, bootstrapRegistrationBody('stalesetupcomplete')); - await expectUserACLs(account.user_id, []); - + const account = await registerUser(harness, bootstrapRegistrationBody('setupopenowner')); + await expectUserACLs(account.user_id, [AdminACLs.WILDCARD]); await createBuilder(harness, account.token) .patch('/admin/instance/config') .body({app_public: {setup: {configured: true}}}) .execute(); - - await expectUserACLs(account.user_id, [AdminACLs.WILDCARD]); - await createBuilder(harness, account.token).get('/admin/instance/config').execute(); + await expect(instanceConfigRepository.getAppPublicConfig()).resolves.toMatchObject({ + setup: {configured: true}, + }); }); }); it('derives username from display name when username is omitted', async () => { @@ -237,3 +290,303 @@ describe('Auth registration', () => { expect(me.email).toBe('integration-case-store-email@example.com'); }); }); + +describe('Setup token registration', () => { + let harness: ApiTestHarness; + beforeAll(async () => { + harness = await createAuthHarness(); + }); + beforeEach(async () => { + await harness.reset(); + }); + afterAll(async () => { + await harness?.shutdown(); + }); + + async function register( + body: Record, + ipAddress?: string, + ): Promise<{status: number; json: SetupTokenRegistrationResponse}> { + const builder = createBuilderWithoutAuth(harness).post('/auth/register').body(body); + if (ipAddress) builder.header('x-forwarded-for', ipAddress); + const {response, json} = await builder.executeRaw(); + return {status: response.status, json}; + } + + function setupTokenErrors(json: SetupTokenRegistrationResponse): Array<{path: string; code: string}> { + return (json.errors ?? []).map(({path, code}) => ({path, code})); + } + + async function registerAdmin(prefix: string, ipAddress?: string): Promise { + const {status, json} = await register({...bootstrapRegistrationBody(prefix), setup_token: SETUP_TOKEN}, ipAddress); + expect(status).toBe(HTTP_STATUS.OK); + return json.user_id!; + } + + it('refuses a registration that sends no setup token', async () => { + await withSetupTokenInstance(async () => { + const body = bootstrapRegistrationBody('tokenless'); + const {status, json} = await register(body); + expect(status).toBe(HTTP_STATUS.BAD_REQUEST); + expect(setupTokenErrors(json)).toEqual([{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED}]); + expect(await getUserRepository().findByEmail(body.email as string)).toBeNull(); + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false); + }); + }); + + it('refuses a registration that sends a wrong setup token', async () => { + await withSetupTokenInstance(async () => { + const body = bootstrapRegistrationBody('wrongtoken'); + const {status, json} = await register({...body, setup_token: WRONG_SETUP_TOKEN}); + expect(status).toBe(HTTP_STATUS.BAD_REQUEST); + expect(setupTokenErrors(json)).toEqual([{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID}]); + expect(await getUserRepository().findByEmail(body.email as string)).toBeNull(); + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false); + }); + }); + + it('makes the registration that presents the setup token the admin', async () => { + await withSetupTokenInstance(async () => { + const userId = await registerAdmin('tokenadmin'); + await expectUserACLs(userId, [AdminACLs.WILDCARD]); + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true); + expect((await getInstanceConfigRepository().getConfig(SETUP_TOKEN_HASH_KEY)) ?? '').toBe(''); + }); + }); + + it('accepts a setup token padded with whitespace', async () => { + await withSetupTokenInstance(async () => { + const {status, json} = await register({ + ...bootstrapRegistrationBody('paddedtoken'), + setup_token: ` ${SETUP_TOKEN}\n`, + }); + expect(status).toBe(HTTP_STATUS.OK); + await expectUserACLs(json.user_id!, [AdminACLs.WILDCARD]); + }); + }); + + it('ignores the setup token once the admin exists', async () => { + await withSetupTokenInstance(async () => { + await registerAdmin('firstadmin'); + const tokenless = await register(bootstrapRegistrationBody('aftertokenless')); + expect(tokenless.status).toBe(HTTP_STATUS.OK); + await expectUserACLs(tokenless.json.user_id!, []); + const wrong = await register({...bootstrapRegistrationBody('afterwrong'), setup_token: WRONG_SETUP_TOKEN}); + expect(wrong.status).toBe(HTTP_STATUS.OK); + await expectUserACLs(wrong.json.user_id!, []); + const repeated = await register({...bootstrapRegistrationBody('afterrepeat'), setup_token: SETUP_TOKEN}); + expect(repeated.status).toBe(HTTP_STATUS.OK); + await expectUserACLs(repeated.json.user_id!, []); + }); + }); + + it('refuses an unclaimed registration that sends no setup token', async () => { + await withSetupTokenInstance(async () => { + const {status, json} = await register({global_name: 'Unclaimed Visitor'}); + expect(status).toBe(HTTP_STATUS.BAD_REQUEST); + expect(setupTokenErrors(json)).toEqual([{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED}]); + }); + }); + + it('creates a plain account for an unclaimed registration that presents the setup token', async () => { + await withSetupTokenInstance(async () => { + const {status, json} = await register({global_name: 'Unclaimed Operator', setup_token: SETUP_TOKEN}); + expect(status).toBe(HTTP_STATUS.OK); + await expectUserACLs(json.user_id!, []); + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false); + const userId = await registerAdmin('claimedoperator'); + await expectUserACLs(userId, [AdminACLs.WILDCARD]); + }); + }); + + it.each(['approval', 'closed'] as const)( + 'lets the setup token holder register as admin while registration is %s', + async (mode) => { + await withSetupTokenInstance(async () => { + await getInstanceConfigRepository().setRegistrationConfig({mode}); + const {status, json} = await register({...bootstrapRegistrationBody(`mode${mode}`), setup_token: SETUP_TOKEN}); + expect(status).toBe(HTTP_STATUS.OK); + expect(json.registration_pending_approval).toBeUndefined(); + expect(json.token).toBeTruthy(); + await expectUserACLs(json.user_id!, [AdminACLs.WILDCARD]); + await expect(getInstanceConfigRepository().getPendingRegistrations()).resolves.toEqual([]); + }); + }, + ); + + it('reports the missing setup token before any other registration check', async () => { + await withSetupTokenInstance(async () => { + await getInstanceConfigRepository().setAppPublicConfig({legal: {terms_url: 'https://example.com/terms'}}); + const body = {...bootstrapRegistrationBody('noconsent'), consent: false}; + const tokenless = await register(body); + expect(tokenless.status).toBe(HTTP_STATUS.BAD_REQUEST); + expect(setupTokenErrors(tokenless.json)).toEqual([ + {path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED}, + ]); + const verified = await register({...body, setup_token: SETUP_TOKEN}); + expect(verified.status).toBe(HTTP_STATUS.BAD_REQUEST); + expect(setupTokenErrors(verified.json)).toEqual([ + {path: 'consent', code: ValidationErrorCodes.MUST_AGREE_TO_TOS_AND_PRIVACY_POLICY}, + ]); + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false); + }); + }); + + it('locks an address out after ten wrong setup tokens', async () => { + await withSetupTokenInstance(async () => { + getConfig().dev.relaxRegistrationRateLimits = false; + const attacker = '203.0.113.10'; + const operator = '203.0.113.20'; + for (let attempt = 0; attempt < 3; attempt++) { + const tokenless = await register(bootstrapRegistrationBody(`limitmissing${attempt}`), attacker); + expect(setupTokenErrors(tokenless.json)).toEqual([ + {path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED}, + ]); + } + for (let attempt = 0; attempt < 10; attempt++) { + const wrong = await register( + {...bootstrapRegistrationBody(`limitwrong${attempt}`), setup_token: WRONG_SETUP_TOKEN}, + attacker, + ); + expect(wrong.status).toBe(HTTP_STATUS.BAD_REQUEST); + expect(setupTokenErrors(wrong.json)).toEqual([ + {path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID}, + ]); + } + const eleventh = await register( + {...bootstrapRegistrationBody('limiteleventh'), setup_token: WRONG_SETUP_TOKEN}, + attacker, + ); + expect(eleventh.status).toBe(TOO_MANY_REQUESTS); + const lockedOut = await register( + {...bootstrapRegistrationBody('limitlocked'), setup_token: SETUP_TOKEN}, + attacker, + ); + expect(lockedOut.status).toBe(TOO_MANY_REQUESTS); + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false); + const userId = await registerAdmin('limitoperator', operator); + await expectUserACLs(userId, [AdminACLs.WILDCARD]); + }); + }); + + it('lets the setup token holder past the registration limiter of their address', async () => { + await withSetupTokenInstance(async () => { + getConfig().dev.relaxRegistrationRateLimits = false; + const operator = '203.0.113.30'; + const registrationLimit = { + identifier: `registration:ip:${getSameIpDecisionKey(operator) ?? operator}`, + maxAttempts: 3, + windowMs: ms('1 hour'), + }; + for (let attempt = 0; attempt < 3; attempt++) { + await getRateLimitService().checkLimit(registrationLimit); + } + const userId = await registerAdmin('limitedoperator', operator); + await expectUserACLs(userId, [AdminACLs.WILDCARD]); + const next = await register(bootstrapRegistrationBody('limitedmember'), operator); + expect(next.status).toBe(TOO_MANY_REQUESTS); + }); + }); + + it('ties the admin grant to the setup token on an instance configured through the environment', async () => { + await withSetupTokenInstance(async () => { + getConfig().instance.setup.configured = true; + const tokenless = await register(bootstrapRegistrationBody('envtokenless')); + expect(tokenless.status).toBe(HTTP_STATUS.OK); + await expectUserACLs(tokenless.json.user_id!, []); + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false); + const wrong = await register({...bootstrapRegistrationBody('envwrong'), setup_token: WRONG_SETUP_TOKEN}); + expect(wrong.status).toBe(HTTP_STATUS.BAD_REQUEST); + expect(setupTokenErrors(wrong.json)).toEqual([ + {path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID}, + ]); + const userId = await registerAdmin('envadmin'); + await expectUserACLs(userId, [AdminACLs.WILDCARD]); + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true); + }); + }); + + it('ties the admin grant to the setup token on an instance configured in storage', async () => { + await withSetupTokenInstance(async () => { + await getInstanceConfigRepository().setAppPublicConfig({setup: {configured: true}}); + const tokenless = await register(bootstrapRegistrationBody('storedtokenless')); + expect(tokenless.status).toBe(HTTP_STATUS.OK); + await expectUserACLs(tokenless.json.user_id!, []); + const userId = await registerAdmin('storedadmin'); + await expectUserACLs(userId, [AdminACLs.WILDCARD]); + }); + }); + + it('leaves the hosted service alone even with a setup token configured', async () => { + await withSetupTokenInstance(async () => { + getConfig().instance.selfHosted = false; + await getInstanceConfigRepository().setAppPublicConfig({setup: {configured: false}}); + const {status, json} = await register(bootstrapRegistrationBody('hostedtokenless')); + expect(status).toBe(HTTP_STATUS.OK); + await expectUserACLs(json.user_id!, [AdminACLs.WILDCARD]); + }); + }); + + it('releases the admin claim when the account cannot be created', async () => { + await withSetupTokenInstance(async () => { + const create = vi.spyOn(getUserRepository(), 'create').mockRejectedValueOnce(new Error('insert failed')); + try { + const failed = await register({...bootstrapRegistrationBody('failedadmin'), setup_token: SETUP_TOKEN}); + expect(failed.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR); + expect(create).toHaveBeenCalledTimes(1); + } finally { + create.mockRestore(); + } + await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false); + const tokenless = await register(bootstrapRegistrationBody('failedtokenless')); + expect(setupTokenErrors(tokenless.json)).toEqual([ + {path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED}, + ]); + const userId = await registerAdmin('retriedadmin'); + await expectUserACLs(userId, [AdminACLs.WILDCARD]); + await expect(getInstanceConfigRepository().getConfig('admin_bootstrap_user_id')).resolves.toBe(userId); + }); + }); + + it('burns a generated setup token once the admin exists', async () => { + await withSetupTokenInstance(async () => { + const config = getConfig(); + const originalNodeEnv = config.nodeEnv; + config.instance.setup.token = null; + config.nodeEnv = 'production'; + try { + const repository = getInstanceConfigRepository(); + const generated = await repository.createSetupTokenIfAbsent(); + expect(generated).not.toBeNull(); + const wrong = await register({...bootstrapRegistrationBody('genwrong'), setup_token: SETUP_TOKEN}); + expect(setupTokenErrors(wrong.json)).toEqual([ + {path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID}, + ]); + const {status, json} = await register({...bootstrapRegistrationBody('genadmin'), setup_token: generated}); + expect(status).toBe(HTTP_STATUS.OK); + await expectUserACLs(json.user_id!, [AdminACLs.WILDCARD]); + expect(await repository.getConfig(SETUP_TOKEN_HASH_KEY)).toBe(''); + await expect(repository.verifySetupToken(generated)).resolves.toBe(false); + } finally { + config.nodeEnv = originalNodeEnv; + } + }); + }); + + it('burns the stored setup token when an admin marks setup as configured', async () => { + await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => { + const repository = getInstanceConfigRepository(); + await repository.setAppPublicConfig({setup: {configured: false}}); + const account = await registerUser(harness, bootstrapRegistrationBody('configuringadmin')); + await repository.releaseAdminBootstrap(); + const generated = await repository.createSetupTokenIfAbsent(); + await expect(repository.verifySetupToken(generated)).resolves.toBe(true); + await createBuilder(harness, account.token) + .patch('/admin/instance/config') + .body({app_public: {setup: {configured: true}}}) + .execute(); + await expect(repository.isAdminBootstrapped()).resolves.toBe(true); + await expect(repository.verifySetupToken(generated)).resolves.toBe(false); + }); + }); +}); diff --git a/fluxer_api/src/api/config/APIConfig.ts b/fluxer_api/src/api/config/APIConfig.ts index 6ffd7b6e8..2bdf8f044 100644 --- a/fluxer_api/src/api/config/APIConfig.ts +++ b/fluxer_api/src/api/config/APIConfig.ts @@ -283,6 +283,7 @@ export interface APIConfig { }; setup: { configured: boolean; + token: string | null; }; accountIdentity: AccountIdentityMode | null; tagStyle: TagStyle | null; diff --git a/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts b/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts index 61d3d9c9c..981c9d230 100644 --- a/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts +++ b/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createHash} from 'node:crypto'; +import {createUserID} from '@app/api/BrandedTypes'; import {Config, getConfig} from '@app/api/Config'; import type {CassandraQueryExecutorForTesting} from '@app/api/database/CassandraQueryExecution'; import {setCassandraQueryExecutorForTesting} from '@app/api/database/CassandraQueryExecution'; @@ -14,6 +15,7 @@ import { } from '@app/api/instance/InstanceConfigRepository'; import {getLegalUrls, setCachedConfiguredLegalUrls} from '@app/api/instance/LegalUrls'; import {getInstanceProductName, setCachedProductName} from '@app/api/instance/ProductName'; +import {ensureSetupToken, isSetupTokenEnforced} from '@app/api/instance/SetupToken'; import {InstanceConfigWriteRaceExecutor} from '@app/api/instance/tests/InstanceConfigWriteRaceExecutor'; import {dockerAvailable, startPostgresKv, stopPostgresKv} from '@app/api/test/DockerTestContainer'; import {InMemoryCassandraQueryExecutor} from '@app/api/test/InMemoryCassandraQueryExecutor'; @@ -38,6 +40,8 @@ const REGISTRATION_CONFIG_KEY = 'registration_config'; const REGISTRATION_URLS_KEY = 'registration_urls'; const REGISTRATION_PENDING_APPROVALS_KEY = 'registration_pending_approvals'; const POSTGRES_KV_TABLE = 'kv_instance_config_races'; +const SETUP_TOKEN_HASH_KEY = 'setup_token_hash'; +const ENV_SETUP_TOKEN = 'setup-token-for-tests-0123456789-abcdefghijklmnopqrstuvwxyz'; describe('InstanceConfigRepository', () => { const repositories: Array = []; @@ -945,6 +949,271 @@ function registrationUrlParams(maxUses: number | null) { }; } +describe('InstanceConfigRepository setup token', () => { + const repositories: Array = []; + const config = getConfig(); + let executor: InstanceConfigWriteRaceExecutor; + let kvProvider: MockKVProvider; + let originalSelfHosted: boolean; + let originalNodeEnv: typeof config.nodeEnv; + let originalSetupToken: typeof config.instance.setup.token; + let originalSetupConfigured: boolean; + + beforeEach(() => { + executor = new InstanceConfigWriteRaceExecutor(new InMemoryCassandraQueryExecutor()); + setCassandraQueryExecutorForTesting(executor); + kvProvider = new MockKVProvider(); + originalSelfHosted = config.instance.selfHosted; + originalNodeEnv = config.nodeEnv; + originalSetupToken = config.instance.setup.token; + originalSetupConfigured = config.instance.setup.configured; + }); + + afterEach(() => { + config.instance.selfHosted = originalSelfHosted; + config.nodeEnv = originalNodeEnv; + config.instance.setup.token = originalSetupToken; + config.instance.setup.configured = originalSetupConfigured; + for (const repository of repositories) { + repository.shutdown(); + } + repositories.length = 0; + setCassandraQueryExecutorForTesting(new InMemoryCassandraQueryExecutor()); + }); + + function createPod(): InstanceConfigRepository { + const repository = new InstanceConfigRepository(kvProvider); + repositories.push(repository); + return repository; + } + + function useSelfHostedProduction(): void { + config.instance.selfHosted = true; + config.nodeEnv = 'production'; + config.instance.setup.token = null; + config.instance.setup.configured = false; + } + + function sha256(value: string): string { + return createHash('sha256').update(value).digest('hex'); + } + + const accountExists = async () => true; + const accountMissing = async () => false; + + it('generates one setup token when two pods start together and stores only its hash', async () => { + const tokens = await Promise.all([createPod().createSetupTokenIfAbsent(), createPod().createSetupTokenIfAbsent()]); + const generated = tokens.filter((token) => token !== null); + expect(generated).toHaveLength(1); + expect(tokens.filter((token) => token === null)).toHaveLength(1); + const stored = await executor.readDirectly(SETUP_TOKEN_HASH_KEY); + expect(stored).toBe(sha256(generated[0]!)); + expect(stored).not.toBe(generated[0]); + expect(await createPod().createSetupTokenIfAbsent()).toBeNull(); + }); + + it('verifies only the generated setup token', async () => { + const pod = createPod(); + for (const presented of ['anything', '', null, undefined]) { + expect(await pod.verifySetupToken(presented)).toBe(false); + } + const token = await pod.createSetupTokenIfAbsent(); + expect(await pod.verifySetupToken(token)).toBe(true); + expect(await createPod().verifySetupToken(token)).toBe(true); + for (const presented of [`${token}x`, sha256(token!), '', null, undefined]) { + expect(await pod.verifySetupToken(presented)).toBe(false); + } + }); + + it('takes the environment setup token over a generated one', async () => { + const pod = createPod(); + const generated = await pod.createSetupTokenIfAbsent(); + config.instance.setup.token = ENV_SETUP_TOKEN; + expect(await pod.verifySetupToken(ENV_SETUP_TOKEN)).toBe(true); + expect(await pod.verifySetupToken(generated)).toBe(false); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe(sha256(generated!)); + }); + + it('burns the stored setup token and leaves it alone when the admin bootstrap is only claimed', async () => { + useSelfHostedProduction(); + const pod = createPod(); + const token = await pod.createSetupTokenIfAbsent(); + await pod.claimAdminBootstrap(); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe(sha256(token!)); + await pod.burnSetupToken(); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe(''); + expect(await pod.verifySetupToken(token)).toBe(false); + expect(await pod.verifySetupToken('')).toBe(false); + await ensureSetupToken(pod, accountExists); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe(''); + }); + + it('writes nothing when there is no stored setup token to burn', async () => { + await createPod().burnSetupToken(); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull(); + }); + + it('releases a claimed admin bootstrap so it can be claimed again', async () => { + const pod = createPod(); + await pod.releaseAdminBootstrap(); + expect(await executor.readDirectly('admin_bootstrapped')).toBeNull(); + expect(await pod.claimAdminBootstrap()).toBe(true); + expect(await pod.isAdminBootstrapped()).toBe(true); + await pod.releaseAdminBootstrap(); + expect(await pod.isAdminBootstrapped()).toBe(false); + expect(await createPod().isAdminBootstrapped()).toBe(false); + expect(await pod.claimAdminBootstrap()).toBe(true); + expect(await pod.claimAdminBootstrap()).toBe(false); + }); + + it('keeps a won admin bootstrap claim when the refresh cannot be published', async () => { + const pod = createPod(); + await pod.isAdminBootstrapped(); + vi.spyOn(kvProvider, 'publish').mockRejectedValueOnce(new Error('kv down')); + expect(await pod.claimAdminBootstrap()).toBe(true); + expect(await pod.isAdminBootstrapped()).toBe(true); + expect(await executor.readDirectly('admin_bootstrapped')).toBe('true'); + }); + + it('enforces the setup token only on self-hosted instances that run in production or configure one', async () => { + const pod = createPod(); + const gate = async () => ({ + enforced: isSetupTokenEnforced(config), + open: await pod.isSetupTokenGateOpen(), + required: await pod.isSetupTokenRequired(), + }); + config.instance.setup.configured = false; + config.instance.selfHosted = false; + config.nodeEnv = 'production'; + config.instance.setup.token = ENV_SETUP_TOKEN; + expect(await gate()).toEqual({enforced: false, open: false, required: false}); + config.instance.selfHosted = true; + config.nodeEnv = 'development'; + config.instance.setup.token = null; + expect(await gate()).toEqual({enforced: false, open: false, required: false}); + config.instance.setup.token = ENV_SETUP_TOKEN; + expect(await gate()).toEqual({enforced: true, open: true, required: true}); + config.nodeEnv = 'production'; + config.instance.setup.token = null; + expect(await gate()).toEqual({enforced: true, open: true, required: true}); + config.instance.setup.configured = true; + expect(await gate()).toEqual({enforced: true, open: true, required: false}); + config.instance.setup.configured = false; + expect(await gate()).toEqual({enforced: true, open: true, required: true}); + await pod.setAppPublicConfig({setup: {configured: true}}); + expect(await gate()).toEqual({enforced: true, open: true, required: false}); + await pod.claimAdminBootstrap(); + expect(await gate()).toEqual({enforced: true, open: false, required: false}); + }); + + it('stores a setup token on the first production start and keeps it on the next', async () => { + useSelfHostedProduction(); + await ensureSetupToken(createPod(), accountExists); + const stored = await executor.readDirectly(SETUP_TOKEN_HASH_KEY); + expect(stored).toMatch(/^[a-f0-9]{64}$/); + await ensureSetupToken(createPod(), accountExists); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe(stored); + }); + + it('stores a setup token when setup is configured but no admin exists', async () => { + useSelfHostedProduction(); + config.instance.setup.configured = true; + await ensureSetupToken(createPod(), accountExists); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toMatch(/^[a-f0-9]{64}$/); + }); + + it('stores no setup token when one is set in the environment', async () => { + useSelfHostedProduction(); + config.instance.setup.token = ENV_SETUP_TOKEN; + await ensureSetupToken(createPod(), accountExists); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull(); + }); + + it('stores no setup token on the hosted service or in self-hosted development', async () => { + useSelfHostedProduction(); + config.instance.selfHosted = false; + await ensureSetupToken(createPod(), accountExists); + config.instance.selfHosted = true; + config.nodeEnv = 'development'; + await ensureSetupToken(createPod(), accountExists); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull(); + }); + + it('stores no setup token once the admin exists', async () => { + useSelfHostedProduction(); + const pod = createPod(); + await pod.claimAdminBootstrap(); + await ensureSetupToken(pod, accountExists); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull(); + }); + + it('releases an admin bootstrap claim at start when the claiming account was never created', async () => { + useSelfHostedProduction(); + const pod = createPod(); + expect(await pod.claimAdminBootstrap(createUserID(4242n))).toBe(true); + expect(await executor.readDirectly('admin_bootstrap_user_id')).toBe('4242'); + await ensureSetupToken(createPod(), accountExists); + expect(await executor.readDirectly('admin_bootstrapped')).toBe('true'); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull(); + const restarted = createPod(); + await ensureSetupToken(restarted, accountMissing); + expect(await executor.readDirectly('admin_bootstrapped')).toBe('false'); + expect(await restarted.isSetupTokenGateOpen()).toBe(true); + expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toMatch(/^[a-f0-9]{64}$/); + }); + + it('keeps an admin bootstrap claim at start when no claiming account is recorded', async () => { + useSelfHostedProduction(); + const pod = createPod(); + await pod.claimAdminBootstrap(); + expect(await executor.readDirectly('admin_bootstrap_user_id')).toBeNull(); + await ensureSetupToken(createPod(), accountMissing); + expect(await executor.readDirectly('admin_bootstrapped')).toBe('true'); + await pod.releaseAdminBootstrap(); + await pod.claimAdminBootstrap(createUserID(4242n)); + await pod.releaseAdminBootstrap(); + await pod.claimAdminBootstrap(); + expect(await executor.readDirectly('admin_bootstrap_user_id')).toBe(''); + await ensureSetupToken(createPod(), accountMissing); + expect(await executor.readDirectly('admin_bootstrapped')).toBe('true'); + }); + + it('keeps a won admin bootstrap claim when the claiming account cannot be recorded', async () => { + const pod = createPod(); + executor.watch('admin_bootstrap_user_id'); + executor.competeBeforeEachWrite(async () => { + throw new Error('database down'); + }); + expect(await pod.claimAdminBootstrap(createUserID(4242n))).toBe(true); + expect(await pod.isAdminBootstrapped()).toBe(true); + expect(await executor.readDirectly('admin_bootstrapped')).toBe('true'); + expect(await executor.readDirectly('admin_bootstrap_user_id')).toBeNull(); + }); + + it('confirms the admin bootstrap against the database and repairs a stale snapshot', async () => { + const stale = createPod(); + const writer = createPod(); + expect(await stale.isAdminBootstrapped()).toBe(false); + vi.spyOn(kvProvider, 'publish').mockRejectedValueOnce(new Error('kv down')); + await writer.claimAdminBootstrap(); + expect(await stale.isAdminBootstrapped()).toBe(false); + expect(await stale.confirmAdminBootstrapped()).toBe(true); + expect(await stale.isAdminBootstrapped()).toBe(true); + vi.spyOn(kvProvider, 'publish').mockRejectedValueOnce(new Error('kv down')); + await writer.releaseAdminBootstrap(); + expect(await stale.isAdminBootstrapped()).toBe(true); + expect(await stale.confirmAdminBootstrapped()).toBe(false); + expect(await stale.isAdminBootstrapped()).toBe(false); + }); + + it('refuses to start when the setup token cannot be stored', async () => { + useSelfHostedProduction(); + const pod = createPod(); + vi.spyOn(pod, 'createSetupTokenIfAbsent').mockRejectedValueOnce(new Error('database down')); + await expect(ensureSetupToken(pod, accountExists)).rejects.toThrow('database down'); + }); +}); + describe('InstanceConfigRepository concurrent writes', () => { describe('in memory', () => { describeConcurrentInstanceConfigWrites(async () => new InMemoryCassandraQueryExecutor()); diff --git a/fluxer_api/src/api/instance/InstanceConfigRepository.ts b/fluxer_api/src/api/instance/InstanceConfigRepository.ts index d0d15aaa9..6e349d21d 100644 --- a/fluxer_api/src/api/instance/InstanceConfigRepository.ts +++ b/fluxer_api/src/api/instance/InstanceConfigRepository.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import crypto from 'node:crypto'; +import {createUserID, type UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import type {APIConfig, BlueskyOAuthConfig, BlueskyOAuthKeyConfig} from '@app/api/config/APIConfig'; import type {StoredLimitConfig} from '@app/api/constants/LimitConfig'; @@ -32,6 +33,7 @@ import { import {InstanceConfigCache} from '@app/api/instance/InstanceConfigCache'; import {setCachedConfiguredLegalUrls} from '@app/api/instance/LegalUrls'; import {getDefaultProductName, setCachedProductName} from '@app/api/instance/ProductName'; +import {isSetupTokenEnforced} from '@app/api/instance/SetupToken'; import {normalizeSsoAllowedEmailDomains} from '@app/api/instance/SsoConfigValidation'; import {Logger} from '@app/api/Logger'; import {isStoredLimitConfig} from '@app/api/limits/LimitConfigValidation'; @@ -44,6 +46,7 @@ import { import {InstanceConfiguration, Users} from '@app/api/Tables'; import {DEFAULT_DECAY_CONSTANTS, DEFAULT_RENEWAL_CONSTANTS} from '@app/api/utils/AttachmentDecay'; import {isJsonRecord} from '@app/api/utils/JsonBoundaryUtils'; +import {randomBase64Url, secretMatchesHash, sha256Hex} from '@app/api/utils/SecretHash'; import { type AccountIdentityMode, AccountIdentityModes, @@ -120,6 +123,8 @@ const REGISTRATION_URLS_KEY = 'registration_urls'; const REGISTRATION_PENDING_APPROVALS_KEY = 'registration_pending_approvals'; const APP_PUBLIC_CONFIG_KEY = 'app_public_config'; const ADMIN_BOOTSTRAP_KEY = 'admin_bootstrapped'; +const ADMIN_BOOTSTRAP_USER_ID_KEY = 'admin_bootstrap_user_id'; +const SETUP_TOKEN_HASH_KEY = 'setup_token_hash'; const INSTANCE_POLICY_CONFIG_KEY = 'instance_policy_config'; const LIMIT_CONFIG_KEY = 'limit_config'; const INSTANCE_INTEGRATIONS_CONFIG_KEY = 'instance_integrations_config'; @@ -1484,8 +1489,95 @@ export class InstanceConfigRepository { return (await this.getConfig(ADMIN_BOOTSTRAP_KEY)) === 'true'; } - async markAdminBootstrapped(): Promise { - await this.setConfig(ADMIN_BOOTSTRAP_KEY, 'true'); + async confirmAdminBootstrapped(): Promise { + const cache = this.configCache; + const stored = (await this.fetchConfigFromDatabase(ADMIN_BOOTSTRAP_KEY)) === 'true'; + if (stored !== (await this.isAdminBootstrapped())) cache.update(ADMIN_BOOTSTRAP_KEY, stored ? 'true' : 'false'); + return stored; + } + + async claimAdminBootstrap(userId: UserID | null = null): Promise { + const cache = this.configCache; + const {written} = await this.compareAndSetStoredValue(cache, ADMIN_BOOTSTRAP_KEY, (raw) => ({ + value: raw === 'true' ? null : 'true', + result: undefined, + })); + if (!written) return false; + await this.recordAdminBootstrapClaimant(cache, userId); + await this.publishAdminBootstrapRefresh(cache.sourceId); + return true; + } + + private async recordAdminBootstrapClaimant(cache: InstanceConfigCache, userId: UserID | null): Promise { + const claimant = userId?.toString() ?? ''; + try { + await this.compareAndSetStoredValue(cache, ADMIN_BOOTSTRAP_USER_ID_KEY, (raw) => ({ + value: (raw ?? '') === claimant ? null : claimant, + result: undefined, + })); + } catch (error) { + Logger.error({error}, 'Could not record which account claimed the admin bootstrap'); + } + } + + async getUnconfirmedAdminBootstrapClaimant(): Promise { + const [bootstrapped, claimant] = await Promise.all([ + this.fetchConfigFromDatabase(ADMIN_BOOTSTRAP_KEY), + this.fetchConfigFromDatabase(ADMIN_BOOTSTRAP_USER_ID_KEY), + ]); + if (bootstrapped !== 'true' || !claimant || !/^\d+$/.test(claimant)) return null; + return createUserID(BigInt(claimant)); + } + + async releaseAdminBootstrap(): Promise { + const cache = this.configCache; + const {written} = await this.compareAndSetStoredValue(cache, ADMIN_BOOTSTRAP_KEY, (raw) => ({ + value: raw === 'true' ? 'false' : null, + result: undefined, + })); + if (written) await this.publishAdminBootstrapRefresh(cache.sourceId); + } + + private async publishAdminBootstrapRefresh(sourceId: string): Promise { + try { + await this.publishRefresh(sourceId); + } catch (error) { + Logger.error( + {error}, + 'Could not publish the admin bootstrap change. Other API processes see it after the next instance config change or a restart', + ); + } + } + + async isSetupTokenGateOpen(): Promise { + return isSetupTokenEnforced(Config) && !(await this.isAdminBootstrapped()); + } + + async isSetupTokenRequired(): Promise { + return (await this.isSetupTokenGateOpen()) && !(await this.getAppPublicConfig()).setup.configured; + } + + async createSetupTokenIfAbsent(): Promise { + const token = randomBase64Url(32); + const {written} = await this.compareAndSetStoredValue(this.configCache, SETUP_TOKEN_HASH_KEY, (raw) => ({ + value: raw ? null : sha256Hex(token), + result: undefined, + })); + return written ? token : null; + } + + async verifySetupToken(presented: string | null | undefined): Promise { + const expectedHash = Config.instance.setup.token + ? sha256Hex(Config.instance.setup.token) + : await this.fetchConfigFromDatabase(SETUP_TOKEN_HASH_KEY); + return secretMatchesHash(presented, expectedHash); + } + + async burnSetupToken(): Promise { + await this.compareAndSetStoredValue(this.configCache, SETUP_TOKEN_HASH_KEY, (raw) => ({ + value: raw ? '' : null, + result: undefined, + })); } async getAccountIdentity(): Promise { diff --git a/fluxer_api/src/api/instance/InstanceController.ts b/fluxer_api/src/api/instance/InstanceController.ts index 1707169c3..e7e1da0d7 100644 --- a/fluxer_api/src/api/instance/InstanceController.ts +++ b/fluxer_api/src/api/instance/InstanceController.ts @@ -9,11 +9,13 @@ import { isDiscoveryNotModified, nextDiscoveryValidators, } from '@app/api/instance/DiscoveryValidators'; +import {assertSetupToken} from '@app/api/instance/SetupToken'; import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware'; import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware'; import {RateLimitConfigs} from '@app/api/RateLimitConfig'; import {isBillingActive, isPremiumTieringActive, isStripeServiceable} from '@app/api/stripe/BillingConfigCache'; import type {HonoEnv} from '@app/api/types/HonoEnv'; +import {requireConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import {Validator} from '@app/api/Validator'; import {AccountIdentityModes, TagStyles} from '@fluxer/constants/src/AccountIdentityConstants'; import {API_CODE_VERSION} from '@fluxer/constants/src/AppConstants'; @@ -130,10 +132,13 @@ export function InstanceController(app: Hono) { instanceConfigRepository.getDomainMigrationConfig(), instanceConfigRepository.getAccountIdentity(), ]); - const accountIdentityLocked = + const openSetup = Config.instance.selfHosted && !appPublicConfig.setup.configured - ? await instanceConfigRepository.isAccountIdentityLocked() - : undefined; + ? { + account_identity_locked: await instanceConfigRepository.isAccountIdentityLocked(), + setup_token_required: await instanceConfigRepository.isSetupTokenRequired(), + } + : {}; const discovery = buildDiscoveryResponse( buildDiscoveryStaticInput( gifService, @@ -142,7 +147,7 @@ export function InstanceController(app: Hono) { setup: { ...appPublicConfig.setup, admin_url: Config.endpoints.admin || null, - ...(accountIdentityLocked === undefined ? {} : {account_identity_locked: accountIdentityLocked}), + ...openSetup, }, }, { @@ -187,15 +192,23 @@ export function InstanceController(app: Hono) { security: [], tags: ['Instance'], description: - 'Sets how people sign in on a new self-hosted instance, and for email sign-in whether usernames are unique with no tag. Username sign-in always uses unique usernames. It works only before setup is finished and before the first account exists. After that it fails with ACCOUNT_IDENTITY_LOCKED.', + 'Sets how people sign in on a new self-hosted instance, and for email sign-in whether usernames are unique with no tag. Username sign-in always uses unique usernames. It works only before setup is finished and before the first account exists. After that it fails with ACCOUNT_IDENTITY_LOCKED. While the instance requires a setup token, the request must send it as setup_token.', }), async (ctx) => { - const {mode, tag_style} = ctx.req.valid('json'); + const {mode, tag_style, setup_token} = ctx.req.valid('json'); + const instanceConfigRepository = ctx.get('instanceConfigRepository'); + const {config, rateLimit, cache} = ctx.get('apiContext').services; + await assertSetupToken({ + rateLimit, + config, + repository: instanceConfigRepository, + presented: setup_token, + clientIp: requireConfiguredClientIp(ctx.req.raw, config.proxy), + }); if (mode === AccountIdentityModes.USERNAME && tag_style === TagStyles.RANDOM) { throw InputValidationError.fromCode('tag_style', ValidationErrorCodes.TAG_STYLE_REQUIRES_EMAIL_SIGN_IN); } - const instanceConfigRepository = ctx.get('instanceConfigRepository'); - const identity = await withAccountIdentitySetupLock(ctx.get('apiContext').services.cache, async () => { + const identity = await withAccountIdentitySetupLock(cache, async () => { if (await instanceConfigRepository.isAccountIdentityLocked()) { throw new AccountIdentityLockedError(); } diff --git a/fluxer_api/src/api/instance/SetupToken.ts b/fluxer_api/src/api/instance/SetupToken.ts new file mode 100644 index 000000000..f2396d478 --- /dev/null +++ b/fluxer_api/src/api/instance/SetupToken.ts @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type {UserID} from '@app/api/BrandedTypes'; +import {Config} from '@app/api/Config'; +import type {APIConfig} from '@app/api/config/APIConfig'; +import type {InstanceConfigRepository} from '@app/api/instance/InstanceConfigRepository'; +import {Logger} from '@app/api/Logger'; +import {createRateLimitError} from '@app/api/utils/RateLimitUtils'; +import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; +import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; +import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress'; +import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; +import {ms} from 'itty-time'; + +const SETUP_TOKEN_FAILURE_MAX_ATTEMPTS = 10; +const SETUP_TOKEN_FAILURE_WINDOW_MS = ms('30 minutes'); + +export function isSetupTokenEnforced(config: APIConfig): boolean { + return config.instance.selfHosted && (config.nodeEnv === 'production' || config.instance.setup.token !== null); +} + +async function releaseAdminBootstrapOfMissingAccount( + repository: InstanceConfigRepository, + userExists: (userId: UserID) => Promise, +): Promise { + const claimant = await repository.getUnconfirmedAdminBootstrapClaimant(); + if (claimant === null || (await userExists(claimant))) return; + await repository.releaseAdminBootstrap(); + Logger.warn( + {user_id: claimant.toString()}, + 'The account that claimed the admin bootstrap does not exist. The claim is released and the setup token applies again', + ); +} + +export async function ensureSetupToken( + repository: InstanceConfigRepository, + userExists: (userId: UserID) => Promise, +): Promise { + if (!isSetupTokenEnforced(Config)) return; + await releaseAdminBootstrapOfMissingAccount(repository, userExists); + if (!(await repository.isSetupTokenGateOpen())) return; + if (Config.instance.setup.token !== null) { + Logger.info('Setup token read from FLUXER_SETUP_TOKEN'); + return; + } + const token = await repository.createSetupTokenIfAbsent(); + if (token === null) { + Logger.info( + 'A setup token from an earlier start is still unused. Set FLUXER_SETUP_TOKEN to replace it if it is lost', + ); + return; + } + Logger.warn( + {setup_token: token}, + 'Setup token generated. This instance has not recorded an admin account yet. The registration that presents this token becomes an admin. It is shown once', + ); +} + +export async function clearUsedSetupToken(repository: InstanceConfigRepository): Promise { + try { + await repository.burnSetupToken(); + } catch (error) { + Logger.error({error}, 'Could not clear the stored setup token after the admin bootstrap was claimed'); + } +} + +export async function assertSetupToken(params: { + rateLimit: IRateLimitService; + config: APIConfig; + repository: InstanceConfigRepository; + presented: string | null | undefined; + clientIp: string; +}): Promise { + const {rateLimit, config, repository, clientIp} = params; + if (!isSetupTokenEnforced(config)) return false; + const setupConfigured = (await repository.getAppPublicConfig()).setup.configured; + const adminBootstrapped = setupConfigured + ? await repository.isAdminBootstrapped() + : await repository.confirmAdminBootstrapped(); + if (adminBootstrapped) return false; + const token = params.presented?.trim(); + if (!token) { + if (!setupConfigured) { + throw InputValidationError.fromCode('setup_token', ValidationErrorCodes.SETUP_TOKEN_REQUIRED); + } + return false; + } + const failureLimit = config.dev.relaxRegistrationRateLimits + ? null + : { + identifier: `setup-token:ip:${getSameIpDecisionKey(clientIp) ?? clientIp}`, + maxAttempts: SETUP_TOKEN_FAILURE_MAX_ATTEMPTS, + windowMs: SETUP_TOKEN_FAILURE_WINDOW_MS, + }; + if (failureLimit) { + const failureState = await rateLimit.peekLimit(failureLimit); + if (failureState.remaining === 0) { + throw createRateLimitError({ + ...failureState, + allowed: false, + retryAfter: Math.ceil(failureLimit.windowMs / failureLimit.maxAttempts / 1000), + }); + } + } + if (!(await repository.verifySetupToken(token))) { + if (failureLimit) await rateLimit.checkLimit(failureLimit); + throw InputValidationError.fromCode('setup_token', ValidationErrorCodes.SETUP_TOKEN_INVALID); + } + return true; +} diff --git a/fluxer_api/src/api/instance/tests/AccountIdentityMode.test.ts b/fluxer_api/src/api/instance/tests/AccountIdentityMode.test.ts index 00f046fdc..1a4bead53 100644 --- a/fluxer_api/src/api/instance/tests/AccountIdentityMode.test.ts +++ b/fluxer_api/src/api/instance/tests/AccountIdentityMode.test.ts @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import {createHash} from 'node:crypto'; import {createAuthHarness, createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils'; import {getConfig} from '@app/api/Config'; import {getCachedAccountIdentity, getCachedAccountIdentityMode} from '@app/api/instance/AccountIdentityModeCache'; @@ -14,10 +15,12 @@ import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'; const ACCOUNT_IDENTITY_CONFIG_KEY = 'account_identity_config'; +const SETUP_TOKEN = 'setup-token-for-tests-0123456789-abcdefghijklmnopqrstuvwxyz'; +const WRONG_SETUP_TOKEN = 'wrong-setup-token-0123456789-abcdefghijklmnopqrstuvwxyz'; interface DiscoveryResponse { features: {account_identity: string; emails_enabled: boolean; tag_style: string}; - app_public: {setup: {configured: boolean; account_identity_locked?: boolean}}; + app_public: {setup: {configured: boolean; account_identity_locked?: boolean; setup_token_required?: boolean}}; } interface AdminInstanceConfigResponse { @@ -33,6 +36,7 @@ describe('Account identity mode', () => { let originalSetupConfigured: boolean; let originalAccountIdentity: typeof config.instance.accountIdentity; let originalTagStyle: typeof config.instance.tagStyle; + let originalSetupToken: typeof config.instance.setup.token; beforeAll(async () => { harness = await createAuthHarness(); @@ -45,6 +49,7 @@ describe('Account identity mode', () => { originalSetupConfigured = config.instance.setup.configured; originalAccountIdentity = config.instance.accountIdentity; originalTagStyle = config.instance.tagStyle; + originalSetupToken = config.instance.setup.token; }); afterEach(() => { vi.restoreAllMocks(); @@ -53,6 +58,7 @@ describe('Account identity mode', () => { config.instance.setup.configured = originalSetupConfigured; config.instance.accountIdentity = originalAccountIdentity; config.instance.tagStyle = originalTagStyle; + config.instance.setup.token = originalSetupToken; repository.clearCacheForTesting(); }); afterAll(async () => { @@ -239,7 +245,7 @@ describe('Account identity mode', () => { it('stamps email when the admin was bootstrapped', async () => { useFreshSelfHostedInstance(); - await repository.markAdminBootstrapped(); + await repository.claimAdminBootstrap(); await repository.ensureAccountIdentityMode(); expect(await readStoredRow()).toMatchObject({mode: AccountIdentityModes.EMAIL, source: 'existing_instance'}); }); @@ -321,7 +327,7 @@ describe('Account identity mode', () => { it('skips the users probe when a cheaper signal already decides', async () => { useFreshSelfHostedInstance(); const probe = vi.spyOn(repository as unknown as {hasAnyUser: () => Promise}, 'hasAnyUser'); - await repository.markAdminBootstrapped(); + await repository.claimAdminBootstrap(); await repository.ensureAccountIdentityMode(); expect(await readStoredRow()).toMatchObject({mode: AccountIdentityModes.EMAIL, source: 'existing_instance'}); expect(await repository.isAccountIdentityLocked()).toBe(true); @@ -540,6 +546,77 @@ describe('Account identity mode', () => { }); }); + describe('setup token', () => { + async function putModeWithToken(setupToken?: string) { + return await createBuilderWithoutAuth<{ + mode?: string; + tag_style?: string; + errors?: Array<{path: string; code: string}>; + }>(harness) + .put('/instance/setup/account-identity') + .body( + setupToken === undefined + ? {mode: AccountIdentityModes.EMAIL} + : {mode: AccountIdentityModes.EMAIL, setup_token: setupToken}, + ) + .executeRaw(); + } + + it('reports in discovery whether registration needs the setup token', async () => { + useFreshSelfHostedInstance(); + await repository.ensureAccountIdentityMode(); + expect((await fetchDiscovery()).app_public.setup.setup_token_required).toBe(false); + config.instance.setup.token = SETUP_TOKEN; + const {text, json} = await createBuilderWithoutAuth(harness) + .get('/.well-known/fluxer') + .executeRaw(); + expect(json.app_public.setup.setup_token_required).toBe(true); + expect(text).not.toContain(SETUP_TOKEN); + expect(text).not.toContain(createHash('sha256').update(SETUP_TOKEN).digest('hex')); + await repository.claimAdminBootstrap(); + expect((await fetchDiscovery()).app_public.setup.setup_token_required).toBe(false); + await repository.setAppPublicConfig({setup: {configured: true}}); + expect((await fetchDiscovery()).app_public.setup).not.toHaveProperty('setup_token_required'); + }); + + it('leaves the setup token out of discovery on the hosted service', async () => { + config.instance.selfHosted = false; + config.instance.setup.token = SETUP_TOKEN; + await repository.setAppPublicConfig({setup: {configured: false}}); + expect((await fetchDiscovery()).app_public.setup).not.toHaveProperty('setup_token_required'); + }); + + it('needs the setup token to choose the sign-in method while one is required', async () => { + useFreshSelfHostedInstance(); + await repository.ensureAccountIdentityMode(); + config.instance.setup.token = SETUP_TOKEN; + const missing = await putModeWithToken(); + expect(missing.response.status).toBe(400); + expect(missing.json.errors?.map(({path, code}) => ({path, code}))).toEqual([ + {path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED}, + ]); + const wrong = await putModeWithToken(WRONG_SETUP_TOKEN); + expect(wrong.response.status).toBe(400); + expect(wrong.json.errors?.map(({path, code}) => ({path, code}))).toEqual([ + {path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID}, + ]); + expect(await readStoredRow()).toMatchObject({mode: AccountIdentityModes.USERNAME}); + const correct = await putModeWithToken(SETUP_TOKEN); + expect(correct.response.status).toBe(200); + expect(correct.json).toEqual({mode: AccountIdentityModes.EMAIL, tag_style: 'none'}); + expect(await readStoredRow()).toMatchObject({mode: AccountIdentityModes.EMAIL, source: 'setup'}); + }); + + it('chooses the sign-in method without a setup token when none is required', async () => { + useFreshSelfHostedInstance(); + await repository.ensureAccountIdentityMode(); + const tokenless = await putModeWithToken(); + expect(tokenless.response.status).toBe(200); + const ignored = await putModeWithToken(WRONG_SETUP_TOKEN); + expect(ignored.response.status).toBe(200); + }); + }); + describe('admin instance config', () => { async function fetchAdminConfig(token: string): Promise { return await createBuilder(harness, token).get('/admin/instance/config').execute(); diff --git a/fluxer_api/src/api/middleware/ServiceSingletons.ts b/fluxer_api/src/api/middleware/ServiceSingletons.ts index 81b63564d..29ac1dba5 100644 --- a/fluxer_api/src/api/middleware/ServiceSingletons.ts +++ b/fluxer_api/src/api/middleware/ServiceSingletons.ts @@ -47,6 +47,7 @@ import {InstanceConfigRepository} from '@app/api/instance/InstanceConfigReposito import {type LegalUrls, resolveLegalUrls} from '@app/api/instance/LegalUrls'; import {NatsConfigPublisher} from '@app/api/instance/NatsConfigPublisher'; import {getInstanceProductName} from '@app/api/instance/ProductName'; +import {ensureSetupToken} from '@app/api/instance/SetupToken'; import {InviteRepository} from '@app/api/invite/InviteRepository'; import {Logger} from '@app/api/Logger'; import {LimitConfigService} from '@app/api/limits/LimitConfigService'; @@ -482,6 +483,11 @@ export async function initializeServiceSingletons(): Promise { assertServiceSingletonInitializationActive(owner); await getInstanceConfigRepository().ensureAccountIdentityMode(); assertServiceSingletonInitializationActive(owner); + await ensureSetupToken( + getInstanceConfigRepository(), + async (userId) => (await getUserRepository().findUnique(userId)) !== null, + ); + assertServiceSingletonInitializationActive(owner); await limitConfigService.initialize(); assertServiceSingletonInitializationActive(owner); limitConfigService.setAsGlobalInstance(); diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index 4fa9f3c64..8a069c4d9 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -11390,7 +11390,7 @@ "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} } }, - "description": "Sets how people sign in on a new self-hosted instance, and for email sign-in whether usernames are unique with no tag. Username sign-in always uses unique usernames. It works only before setup is finished and before the first account exists. After that it fails with ACCOUNT_IDENTITY_LOCKED.", + "description": "Sets how people sign in on a new self-hosted instance, and for email sign-in whether usernames are unique with no tag. Username sign-in always uses unique usernames. It works only before setup is finished and before the first account exists. After that it fails with ACCOUNT_IDENTITY_LOCKED. While the instance requires a setup token, the request must send it as setup_token.", "requestBody": { "required": true, "content": { @@ -29472,6 +29472,10 @@ "tag_style": { "description": "How usernames are tagged. Defaults to none. Username sign-in accepts only none", "$ref": "#/components/schemas/TagStyleSchema" + }, + "setup_token": { + "description": "Setup token of a new self-hosted instance. Required until the first admin account exists", + "type": ["string", "null"] } }, "required": ["mode"] @@ -32830,6 +32834,10 @@ ], "enum": ["dark", "dark_legacy", "coal", "light", "system"], "type": "string" + }, + "setup_token": { + "description": "Setup token of a new self-hosted instance. Required until the first admin account exists", + "type": ["string", "null"] } } }, @@ -33569,6 +33577,10 @@ "account_identity_locked": { "description": "Present only while a self-hosted instance is unconfigured. True when the sign-in method can no longer change", "type": "boolean" + }, + "setup_token_required": { + "description": "Present only while a self-hosted instance is unconfigured. True when registration needs the setup token", + "type": "boolean" } }, "required": ["configured", "admin_url"], diff --git a/fluxer_api/src/api/stripe/tests/ProductRegistryLegacyPrices.test.ts b/fluxer_api/src/api/stripe/tests/ProductRegistryLegacyPrices.test.ts index 0eae9e94b..e6a0d3571 100644 --- a/fluxer_api/src/api/stripe/tests/ProductRegistryLegacyPrices.test.ts +++ b/fluxer_api/src/api/stripe/tests/ProductRegistryLegacyPrices.test.ts @@ -6,7 +6,7 @@ import {afterAll, beforeAll, describe, expect, test} from 'vitest'; // Mirrors the live BRL/TRY configuration after the repricing: the BRL slots point at the new list // prices, TRY is not configured at all (production has no TRY prices), and every retired price id -// lives only in the legacy map. +// is only in the legacy map. const MOCK_PRICES = { monthlyUsd: 'price_monthly_usd', monthlyBrl: 'price_1TMbqsFPC94Os7FdfElrIIaZ', diff --git a/fluxer_app/src/features/app/components/setup/SelfHostedSetupWizardGate.tsx b/fluxer_app/src/features/app/components/setup/SelfHostedSetupWizardGate.tsx index bcbb6acc9..648c6e22b 100644 --- a/fluxer_app/src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +++ b/fluxer_app/src/features/app/components/setup/SelfHostedSetupWizardGate.tsx @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import {Routes} from '@app/app/Routes'; import * as Modal from '@app/features/app/components/dialogs/Modal'; import styles from '@app/features/app/components/setup/SelfHostedSetupWizardGate.module.css'; import { @@ -39,6 +40,7 @@ import { type ServiceIntegrationDraft, type ServiceSelection, ServicesStep, + SetupTokenStep, SignInMethodStep, ThemeStep, toSmtpTlsMode, @@ -48,15 +50,16 @@ import {resolveAppShellBranding} from '@app/features/app/state/AppShellBranding' import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import Authentication from '@app/features/auth/state/Authentication'; import {AuthRegisterDraftContext, type AuthRegisterFormDraft} from '@app/features/auth/state/AuthRegisterDraftContext'; -import {BACK_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; +import {BACK_DESCRIPTOR, SIGN_OUT_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import {openFilePicker} from '@app/features/messaging/utils/FilePickerUtils'; +import * as RouterUtils from '@app/features/navigation/utils/RouterUtils'; import {AccountScopedWork} from '@app/features/platform/state/AccountScopedWork'; import {isAccountTransitionAbortError} from '@app/features/platform/state/AccountTransitionAbort'; import SessionManager from '@app/features/platform/state/AuthSession'; import {HttpError} from '@app/features/platform/types/EndpointError'; import {Logger} from '@app/features/platform/utils/AppLogger'; import {readBlobAsDataUrl as fileToBase64} from '@app/features/platform/utils/BlobDataUrl'; -import {failureCode} from '@app/features/platform/utils/ResponseInspection'; +import {failureCode, failureValidationErrors} from '@app/features/platform/utils/ResponseInspection'; import Theme from '@app/features/theme/state/Theme'; import {Button} from '@app/features/ui/button/Button'; import FocusRingManager from '@app/features/ui/focus_ring/FocusRingManager'; @@ -132,6 +135,11 @@ const LOAD_ERROR_DESCRIPTOR = msg({ message: 'Could not load the instance configuration. Try reloading the page.', comment: 'Error shown when the setup wizard fails to load the instance configuration.', }); +const NOT_ADMIN_DESCRIPTOR = msg({ + message: 'This account is not an admin of this instance. Sign in with an admin account to finish setup.', + comment: + 'Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows.', +}); const ORIGIN_MISMATCH_DESCRIPTOR = msg({ message: 'The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload.', @@ -460,6 +468,8 @@ export const SelfHostedSetupWizardGate = observer(() => { const [tagStyleChoice, setTagStyleChoice] = useState(() => RuntimeConfig.tagStyle); const [savingSignInMethod, setSavingSignInMethod] = useState(false); const [signInMethodError, setSignInMethodError] = useState(null); + const [setupToken, setSetupToken] = useState(''); + const [setupTokenError, setSetupTokenError] = useState(null); const [creatingRecoveryKit, setCreatingRecoveryKit] = useState(false); const [productName, setProductName] = useState(''); @@ -552,8 +562,7 @@ export const SelfHostedSetupWizardGate = observer(() => { setForceUnauthenticatedSetup(false); }, [authStoreAuthenticated, forceUnauthenticatedSetup]); - const resetStaleSetupSession = useCallback(async () => { - logger.warn('The setup session token was rejected. Clearing the stale local setup session.'); + const resetSetupSession = useCallback(async () => { setForceUnauthenticatedSetup(true); registerFormDraftsRef.current.clear(); setConfig(null); @@ -569,17 +578,24 @@ export const SelfHostedSetupWizardGate = observer(() => { setSmtpTestResult(null); setSignInMethodError(null); setSavingSignInMethod(false); + setSetupToken(''); + setSetupTokenError(null); setCreatingRecoveryKit(false); try { await SessionManager.logout(); } catch (error) { - logger.warn('Failed to fully clear stale setup session', error); + logger.warn('Failed to fully clear the setup session', error); Authentication.handleSessionStart({token: null}); SessionManager.setUserId(null); } Authentication.handleLogout({skipRedirect: true}); }, [clearStepNavigationLock]); + const signOutOfSetup = useCallback(async () => { + await resetSetupSession(); + if (RuntimeConfig.appPublic.setup.setup_token_required !== true) RouterUtils.replaceWith(Routes.LOGIN); + }, [resetSetupSession]); + const hydrateFromConfig = useCallback((next: InstanceConfigResponse) => { setConfig(next); setProductName(next.app_public.branding.product_name); @@ -643,7 +659,12 @@ export const SelfHostedSetupWizardGate = observer(() => { error instanceof HttpError && error.status === 401 ? await classifySetupUnauthorized() : 'unknown'; if (cancelled) return; if (cause === 'stale_session') { - await resetStaleSetupSession(); + logger.warn('The setup session token was rejected. Clearing the stale local setup session.'); + await resetSetupSession(); + return; + } + if (error instanceof HttpError && error.status === 403) { + setLoadError(NOT_ADMIN_DESCRIPTOR); return; } logger.error('Failed to load instance configuration', error); @@ -653,7 +674,7 @@ export const SelfHostedSetupWizardGate = observer(() => { return () => { cancelled = true; }; - }, [isAuthenticated, config, accountWorkAdmitted, hydrateFromConfig, resetStaleSetupSession]); + }, [isAuthenticated, config, accountWorkAdmitted, hydrateFromConfig, resetSetupSession]); const serviceAvailability: ServiceAvailability = useMemo( () => ({ @@ -675,17 +696,20 @@ export const SelfHostedSetupWizardGate = observer(() => { const hasConfig = Boolean(config); const identityLocked = RuntimeConfig.appPublic.setup.account_identity_locked === true; + const setupTokenRequired = RuntimeConfig.appPublic.setup.setup_token_required === true; const syncedSnapshot = wizardSnapshot.context.isAuthenticated !== isAuthenticated || wizardSnapshot.context.hasConfig !== hasConfig || wizardSnapshot.context.accountIdentity !== accountIdentity || - wizardSnapshot.context.identityLocked !== identityLocked + wizardSnapshot.context.identityLocked !== identityLocked || + wizardSnapshot.context.setupTokenRequired !== setupTokenRequired ? transitionSetupWizardSnapshot(wizardSnapshot, { type: 'wizard.sync', isAuthenticated, hasConfig, accountIdentity, identityLocked, + setupTokenRequired, }) : wizardSnapshot; if (syncedSnapshot !== wizardSnapshot) { @@ -712,6 +736,7 @@ export const SelfHostedSetupWizardGate = observer(() => { const canAdvance = useMemo(() => { if (step === 'welcome') return !isAuthenticated || Boolean(config); if (step === 'admin_account' || step === 'loading') return false; + if (step === 'setup_token') return setupToken.trim().length > 0; if (step === 'sign_in_method') return !savingSignInMethod; if (step === 'admin_recovery_kit') return hasRecoveryKit; if (step === 'branding') return !productNameError; @@ -731,6 +756,7 @@ export const SelfHostedSetupWizardGate = observer(() => { integrationDraft, savingSignInMethod, hasRecoveryKit, + setupToken, ]); const goNext = useCallback(() => { @@ -745,6 +771,36 @@ export const SelfHostedSetupWizardGate = observer(() => { setWizardSnapshot((current) => transitionSetupWizardSnapshot(current, {type: 'wizard.back'})); }, [beginStepNavigation]); + const handleSetupTokenChange = useCallback((value: string) => { + setSetupToken(value); + setSetupTokenError(null); + }, []); + + const submittedSetupToken = setupTokenRequired ? setupToken.trim() : undefined; + + const handleSetupTokenRejected = useCallback(async (message: string) => { + setSetupTokenError(message); + try { + await RuntimeConfig.refreshDiscovery(); + } catch (error) { + logger.warn('Failed to refresh discovery after the setup token was rejected', error); + } + const required = RuntimeConfig.appPublic.setup.setup_token_required === true; + setWizardSnapshot((current) => + transitionSetupWizardSnapshot( + transitionSetupWizardSnapshot(current, { + type: 'wizard.sync', + isAuthenticated: current.context.isAuthenticated, + hasConfig: current.context.hasConfig, + accountIdentity: current.context.accountIdentity, + identityLocked: current.context.identityLocked, + setupTokenRequired: required, + }), + {type: 'wizard.goto', step: 'setup_token'}, + ), + ); + }, []); + const submitSignInMethod = useCallback(async () => { if (savingSignInMethod) return; setSavingSignInMethod(true); @@ -754,8 +810,16 @@ export const SelfHostedSetupWizardGate = observer(() => { try { let saved: Awaited> | null = null; try { - saved = await setSetupAccountIdentity(signInMethod, effectiveTagStyle); + saved = await setSetupAccountIdentity(signInMethod, effectiveTagStyle, submittedSetupToken); } catch (error) { + const setupTokenViolation = failureValidationErrors(error)?.find( + (violation) => violation.path === 'setup_token', + ); + if (setupTokenViolation) { + await handleSetupTokenRejected(setupTokenViolation.message); + setSavingSignInMethod(false); + return; + } if (failureCode(error) !== APIErrorCodes.ACCOUNT_IDENTITY_LOCKED) { throw error; } @@ -792,7 +856,7 @@ export const SelfHostedSetupWizardGate = observer(() => { } setSavingSignInMethod(false); goNext(); - }, [savingSignInMethod, signInMethod, tagStyleChoice, i18n, goNext]); + }, [savingSignInMethod, signInMethod, tagStyleChoice, submittedSetupToken, handleSetupTokenRejected, i18n, goNext]); const handleSignInMethodChange = useCallback((mode: AccountIdentityMode) => { setSignInMethod(mode); @@ -1022,6 +1086,15 @@ export const SelfHostedSetupWizardGate = observer(() => { > {i18n._(loadError)}

+ {loadError === NOT_ADMIN_DESCRIPTOR && ( + + )} ) : ( { data-flx="app.setup.self-hosted-setup-wizard-gate.welcome-step" /> )} + {step === 'setup_token' && ( + + )} {step === 'theme' && ( { )} diff --git a/fluxer_app/src/features/app/components/setup/SetupWizardClient.ts b/fluxer_app/src/features/app/components/setup/SetupWizardClient.ts index a2627e3b3..fdac35c53 100644 --- a/fluxer_app/src/features/app/components/setup/SetupWizardClient.ts +++ b/fluxer_app/src/features/app/components/setup/SetupWizardClient.ts @@ -42,8 +42,13 @@ export async function uploadBrandingAsset( export async function setSetupAccountIdentity( mode: InstanceAccountIdentityUpdateRequest['mode'], tagStyle?: InstanceAccountIdentityUpdateRequest['tag_style'], + setupToken?: string, ): Promise { - const body: InstanceAccountIdentityUpdateRequest = tagStyle === undefined ? {mode} : {mode, tag_style: tagStyle}; + const body: InstanceAccountIdentityUpdateRequest = { + mode, + ...(tagStyle === undefined ? {} : {tag_style: tagStyle}), + ...(setupToken ? {setup_token: setupToken} : {}), + }; const response = await http.put(Endpoints.INSTANCE_SETUP_ACCOUNT_IDENTITY, {body}); return response.body; } diff --git a/fluxer_app/src/features/app/components/setup/SetupWizardStateMachine.ts b/fluxer_app/src/features/app/components/setup/SetupWizardStateMachine.ts index 0cf7c1733..932be4ae3 100644 --- a/fluxer_app/src/features/app/components/setup/SetupWizardStateMachine.ts +++ b/fluxer_app/src/features/app/components/setup/SetupWizardStateMachine.ts @@ -5,6 +5,7 @@ import {assign, initialTransition, type SnapshotFrom, setup, transition} from 'x export type WizardStep = | 'welcome' + | 'setup_token' | 'theme' | 'sign_in_method' | 'admin_intro' @@ -28,6 +29,7 @@ type WizardPhase = 'register' | 'loading' | 'configure'; const REGISTER_STEPS: ReadonlyArray = [ 'welcome', + 'setup_token', 'theme', 'sign_in_method', 'admin_intro', @@ -65,11 +67,14 @@ const USERNAME_CONFIGURE_STEPS: ReadonlyArray = [ function phaseSteps( phase: WizardPhase, accountIdentity: AccountIdentityMode, - identityLocked = false, + identityLocked: boolean, + setupTokenRequired: boolean, ): ReadonlyArray { switch (phase) { case 'register': - return identityLocked ? REGISTER_STEPS.filter((step) => step !== 'sign_in_method') : REGISTER_STEPS; + return REGISTER_STEPS.filter( + (step) => (step !== 'sign_in_method' || !identityLocked) && (step !== 'setup_token' || setupTokenRequired), + ); case 'loading': return LOADING_STEPS; case 'configure': @@ -84,17 +89,20 @@ interface SetupWizardMachineContext { hasConfig: boolean; accountIdentity: AccountIdentityMode; identityLocked: boolean; + setupTokenRequired: boolean; } export type SetupWizardMachineEvent = | {type: 'wizard.next'} | {type: 'wizard.back'} + | {type: 'wizard.goto'; step: WizardStep} | { type: 'wizard.sync'; isAuthenticated: boolean; hasConfig: boolean; accountIdentity: AccountIdentityMode; identityLocked: boolean; + setupTokenRequired: boolean; }; export interface SetupWizardModel { @@ -105,18 +113,30 @@ export interface SetupWizardModel { stepIndex: number; } -function advance(context: SetupWizardMachineContext, delta: number): Partial { - const steps = phaseSteps( +function contextSteps(context: SetupWizardMachineContext): ReadonlyArray { + return phaseSteps( derivePhase(context.isAuthenticated, context.hasConfig), context.accountIdentity, context.identityLocked, + context.setupTokenRequired, ); +} + +function advance(context: SetupWizardMachineContext, delta: number): Partial { + const steps = contextSteps(context); const index = steps.indexOf(context.step); const safeIndex = index < 0 ? 0 : index; const nextIndex = Math.min(steps.length - 1, Math.max(0, safeIndex + delta)); return {step: steps[nextIndex], direction: Math.sign(delta)}; } +function jump(context: SetupWizardMachineContext, step: WizardStep): Partial { + const steps = contextSteps(context); + const targetIndex = steps.indexOf(step); + if (targetIndex < 0) return {}; + return {step, direction: Math.sign(targetIndex - steps.indexOf(context.step))}; +} + function targetsPhase(event: SetupWizardMachineEvent, context: SetupWizardMachineContext, phase: WizardPhase): boolean { if (event.type !== 'wizard.sync') return false; return ( @@ -144,9 +164,12 @@ const setupWizardStateMachine = setup({ event.type === 'wizard.sync' ? event.accountIdentity : context.accountIdentity, identityLocked: ({context, event}) => event.type === 'wizard.sync' ? event.identityLocked : context.identityLocked, + setupTokenRequired: ({context, event}) => + event.type === 'wizard.sync' ? event.setupTokenRequired : context.setupTokenRequired, }), goNext: assign(({context}) => advance(context, 1)), goBack: assign(({context}) => advance(context, -1)), + goTo: assign(({context, event}) => (event.type === 'wizard.goto' ? jump(context, event.step) : {})), enterRegister: assign({step: () => REGISTER_STEPS[0], direction: () => 0}), enterLoading: assign({step: () => LOADING_STEPS[0], direction: () => 0}), enterConfigure: assign({step: () => CONFIGURE_STEPS[0], direction: () => 0}), @@ -160,6 +183,7 @@ const setupWizardStateMachine = setup({ hasConfig: false, accountIdentity: AccountIdentityModes.EMAIL, identityLocked: false, + setupTokenRequired: false, }, initial: 'register', on: { @@ -176,6 +200,7 @@ const setupWizardStateMachine = setup({ on: { 'wizard.next': {actions: 'goNext'}, 'wizard.back': {actions: 'goBack'}, + 'wizard.goto': {actions: 'goTo'}, }, }, loading: { @@ -206,7 +231,7 @@ export function transitionSetupWizardSnapshot( export function selectSetupWizardModel(snapshot: SetupWizardSnapshot): SetupWizardModel { const phase = derivePhase(snapshot.context.isAuthenticated, snapshot.context.hasConfig); - const steps = phaseSteps(phase, snapshot.context.accountIdentity, snapshot.context.identityLocked); + const steps = contextSteps(snapshot.context); const stepIndex = steps.indexOf(snapshot.context.step); return { phase, diff --git a/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx b/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx index 23f45c5b4..64093ba48 100644 --- a/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx +++ b/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx @@ -56,6 +56,24 @@ const WELCOME_LANGUAGE_LABEL_DESCRIPTOR = msg({ message: 'Language', comment: 'Label for the setup wizard language selector.', }); +const SETUP_TOKEN_TITLE_DESCRIPTOR = msg({ + message: 'Enter your setup token', + comment: 'Setup wizard title for the step that asks the operator for the setup token of a new instance.', +}); +const SETUP_TOKEN_BODY_DESCRIPTOR = msg({ + message: 'The token proves that you run this instance. Only the person who has it can create the admin account.', + comment: 'Setup wizard body explaining why the operator has to enter the setup token.', +}); +const SETUP_TOKEN_LABEL_DESCRIPTOR = msg({ + message: 'Setup token', + comment: 'Label for the setup token input in the setup wizard.', +}); +const SETUP_TOKEN_HELP_DESCRIPTOR = msg({ + message: + 'The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value.', + comment: + 'Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated.', +}); const THEME_TITLE_DESCRIPTOR = msg({ message: 'Choose a theme', comment: 'Setup wizard title for choosing the initial account theme.', @@ -558,6 +576,37 @@ export const WelcomeStep = observer( }, ); +export const SetupTokenStep = observer( + ({value, onChange, error}: {value: string; onChange: (value: string) => void; error: string | null}) => { + const {i18n} = useLingui(); + return ( +
+ + onChange(event.target.value)} + autoComplete="off" + spellCheck={false} + maxLength={256} + error={error ?? undefined} + footer={ +

+ {i18n._(SETUP_TOKEN_HELP_DESCRIPTOR)} +

+ } + data-flx="app.self-hosted-setup-wizard-gate.setup-token-input" + /> +
+ ); + }, +); + export const ThemeStep = observer( ({theme, onThemeChange}: {theme: ThemeType; onThemeChange: (theme: ThemeType) => void}) => { const {i18n} = useLingui(); @@ -685,31 +734,45 @@ export const AdminIntroStep = observer(() => { ); }); -export const AdminAccountStep = observer(({theme, usernameSignIn}: {theme: ThemeType; usernameSignIn: boolean}) => { - const {i18n} = useLingui(); - return ( -
-
- -
-
- ); -}); +export const AdminAccountStep = observer( + ({ + theme, + usernameSignIn, + setupToken, + onSetupTokenRejected, + }: { + theme: ThemeType; + usernameSignIn: boolean; + setupToken: string | undefined; + onSetupTokenRejected: (message: string) => void; + }) => { + const {i18n} = useLingui(); + return ( +
+
+ +
+
+ ); + }, +); export const AdminRecoveryKitStep = observer( ({hasRecoveryKit, creating, onCreate}: {hasRecoveryKit: boolean; creating: boolean; onCreate: () => void}) => { diff --git a/fluxer_app/src/features/app/domain_migration/DomainMovedStepsModal.tsx b/fluxer_app/src/features/app/domain_migration/DomainMovedStepsModal.tsx index 5eba66f57..6984e9747 100644 --- a/fluxer_app/src/features/app/domain_migration/DomainMovedStepsModal.tsx +++ b/fluxer_app/src/features/app/domain_migration/DomainMovedStepsModal.tsx @@ -20,12 +20,12 @@ const TITLE_DESCRIPTOR = msg({ comment: 'Title of the modal that explains how to install the app from its new domain. productName is the app name.', }); const INTRO_DESCRIPTOR = msg({ - message: '{productName} now lives at {host}. Install it from there, then sign in with this app.', + message: '{productName} is now at {host}. Install it from there, then sign in with this app.', comment: 'Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com.', }); const INSTALL_INTRO_DESCRIPTOR = msg({ - message: '{productName} now lives at {host}. Install it from there.', + message: '{productName} is now at {host}. Install it from there.', comment: 'Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com.', }); diff --git a/fluxer_app/src/features/app/state/RuntimeConfigSnapshot.ts b/fluxer_app/src/features/app/state/RuntimeConfigSnapshot.ts index 9c96373c9..af441d8fa 100644 --- a/fluxer_app/src/features/app/state/RuntimeConfigSnapshot.ts +++ b/fluxer_app/src/features/app/state/RuntimeConfigSnapshot.ts @@ -399,6 +399,7 @@ function readAppPublic(value: unknown): InstanceAppPublic { const setup = readRecord(source.setup, 'appPublic.setup'); const legal = readRecord(source.legal, 'appPublic.legal'); const registration = readRecord(source.registration, 'appPublic.registration'); + const setupTokenRequired = readOptionalBoolean(setup, 'setup_token_required', 'appPublic.setup.setup_token_required'); return { branding: { product_name: readNonEmptyString(branding, 'product_name', 'appPublic.branding.product_name'), @@ -418,6 +419,7 @@ function readAppPublic(value: unknown): InstanceAppPublic { setup: { configured: readBoolean(setup, 'configured', 'appPublic.setup.configured'), admin_url: readNullableAbsoluteHttpUrl(setup, 'admin_url', 'appPublic.setup.admin_url'), + ...(setupTokenRequired === undefined ? {} : {setup_token_required: setupTokenRequired}), }, legal: { terms_url: readNullableAbsoluteHttpUrl(legal, 'terms_url', 'appPublic.legal.terms_url'), diff --git a/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx b/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx index 96c93192c..0ab2b2ca8 100644 --- a/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx +++ b/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx @@ -29,6 +29,7 @@ import { } from '@app/features/i18n/utils/CommonMessageDescriptors'; import {useLocation} from '@app/features/platform/components/router/RouterReact'; import {instanceTargetFromSnapshot} from '@app/features/platform/transport/InstanceHTTP'; +import {failureValidationErrors} from '@app/features/platform/utils/ResponseInspection'; import {Button} from '@app/features/ui/button/Button'; import {createRecoveryKitWithPasswordAndOpen} from '@app/features/user/commands/RecoveryKitCommands'; import {useUsernameSuggestions} from '@app/features/user/hooks/useUsernameSuggestions'; @@ -103,6 +104,8 @@ interface AuthRegisterFormCoreProps { showLegalConsent?: boolean; theme?: ThemeType; offerRecoveryKit?: boolean; + setupToken?: string; + onSetupTokenRejected?: (message: string) => void; } export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ @@ -117,6 +120,8 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ showLegalConsent = true, theme, offerRecoveryKit = false, + setupToken, + onSetupTokenRejected, }: AuthRegisterFormCoreProps) { const {i18n} = useLingui(); const { @@ -128,6 +133,7 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ requireUsername = false, } = fields; const flx = showUsername ? 'auth.flow.auth-register-form-core' : 'auth.flow.auth-minimal-register-form-core'; + const firstFieldName = showEmail ? 'email' : requireUsername ? 'username' : 'global_name'; const location = useLocation(); const draftKey = `register:${location.pathname}${location.search}`; const registrationUrlCode = useMemo(() => { @@ -227,20 +233,35 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ collectDateOfBirth && selectedYear && selectedMonth && selectedDay ? `${selectedYear}-${selectedMonth.padStart(2, '0')}-${selectedDay.padStart(2, '0')}` : undefined; - const response = await AuthenticationCommands.register( - { - global_name: values.global_name || undefined, - username: (requireUsername ? values.username.trim() : values.username) || undefined, - email: showEmail ? values.email : undefined, - password: showPassword ? values.password : undefined, - date_of_birth: dateOfBirth, - consent: effectiveConsent, - invite_code: inviteCode, - registration_url_code: registrationUrlCode, - theme, - }, - registrationTarget, - ); + let response: Awaited>; + try { + response = await AuthenticationCommands.register( + { + global_name: values.global_name || undefined, + username: (requireUsername ? values.username.trim() : values.username) || undefined, + email: showEmail ? values.email : undefined, + password: showPassword ? values.password : undefined, + date_of_birth: dateOfBirth, + consent: effectiveConsent, + invite_code: inviteCode, + registration_url_code: registrationUrlCode, + theme, + setup_token: setupToken || undefined, + }, + registrationTarget, + ); + } catch (error) { + const setupTokenViolation = failureValidationErrors(error)?.find((violation) => violation.path === 'setup_token'); + if (!setupTokenViolation) { + throw error; + } + if (onSetupTokenRejected) { + onSetupTokenRejected(setupTokenViolation.message); + } else { + form.setError(firstFieldName, setupTokenViolation.message); + } + return false; + } if (AuthenticationCommands.isRegistrationPendingApprovalResponse(response)) { clearRegisterFormDraft(draftKey); setPendingApprovalUserId(response.user_id); @@ -273,7 +294,7 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ initialValues, onSubmit: handleRegisterSubmit, redirectPath, - firstFieldName: showEmail ? 'email' : requireUsername ? 'username' : 'global_name', + firstFieldName, }); const setDraftedFormValue = useCallback( (fieldName: string, value: string) => { diff --git a/fluxer_app/src/features/guild/components/modals/GuildPrivacySettingsModal.tsx b/fluxer_app/src/features/guild/components/modals/GuildPrivacySettingsModal.tsx index 13c57a36c..64b52e40e 100644 --- a/fluxer_app/src/features/guild/components/modals/GuildPrivacySettingsModal.tsx +++ b/fluxer_app/src/features/guild/components/modals/GuildPrivacySettingsModal.tsx @@ -24,7 +24,7 @@ const BOT_DIRECT_MESSAGES_DESCRIPTOR = msg({ }); const ALLOW_BOTS_FROM_THIS_COMMUNITY_TO_SEND_YOU_DESCRIPTOR = msg({ message: 'Let bots in this community DM you', - comment: 'Toggle label in the community privacy modal: allow DMs from bots that live in this community.', + comment: 'Toggle label in the community privacy modal: allow DMs from bots that are in this community.', }); export const GuildPrivacySettingsModal = observer(({guildId}: {guildId: string}) => { const {i18n} = useLingui(); diff --git a/fluxer_app/src/features/i18n/locales/ar/messages.po b/fluxer_app/src/features/i18n/locales/ar/messages.po index 1815809e5..d48791aba 100644 --- a/fluxer_app/src/features/i18n/locales/ar/messages.po +++ b/fluxer_app/src/features/i18n/locales/ar/messages.po @@ -1911,6 +1911,16 @@ msgstr "تم نقل {productName} إلى {host}. قم بتثبيت التطبي msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "تم نقل {productName} إلى {host}. افتحه في متصفحك لتثبيت التطبيق الجديد." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} متاح الآن على {host}. قم بتثبيته من هناك، ثم سجّل الدخول باستخدام هذا التطبيق." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "تم نقل {productName} الآن إلى {host}. قم بتثبيته من هناك." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "يحتاج {productName} إلى الوصول إلى الميكروفون msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "يحتاج {productName} إلى إذن تسجيل الشاشة. اسمح بتسجيل الشاشة في إعدادات خصوصية نظام التشغيل الخاص بك وأعد تشغيل التطبيق." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} موجود الآن على {host}. قم بتثبيته من هناك، ثم سجّل الدخول باستخدام هذا التطبيق." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} موجود الآن على {host}. ثبّته من هناك." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "المكالمة متاحة" msgid "Call confirmation" msgstr "تأكيد المكالمة" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "المكالمة في نافذة منفصلة" @@ -14361,6 +14361,11 @@ msgstr "أدخل اسمك" msgid "Enter your password." msgstr "أدخل كلمة المرور الخاصة بك." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "أدخل رمز الإعداد الخاص بك" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "أدخل اسم المستخدم الخاص بك ومفتاح الاسترداد من مجموعة الاسترداد الخاصة بك، ثم اختر كلمة مرور جديدة." @@ -19448,7 +19453,7 @@ msgstr "السعر القديم" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "السماح للأمر /tts بقراءة رسالتك بصوت عالٍ. عند تعطيل هذا الإعداد تبقى هذه الأوامر نصًا عاديًا." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "السماح للبوتات في هذا المجتمع بمراسلتك مباشرةً" @@ -25279,8 +25284,8 @@ msgstr "الخطة" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "الخطط والمزايا والهدايا واشتراكك كلها موجودة في صفحة واحدة." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "الخطط والمزايا والهدايا واشتراكك كلها في صفحة واحدة." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "فتح المستخدم في نافذة منفصلة" msgid "Pop out video" msgstr "فتح الفيديو في نافذة منفصلة" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "في نافذة منفصلة" @@ -30120,6 +30125,11 @@ msgstr "الإعدادات." msgid "Setup key" msgstr "مفتاح الإعداد" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "رمز الإعداد" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "علامة #0000 محجوزة لمشتركي Visionary. لم يعد Visi msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "واجهة برمجة التطبيقات على مصدر مختلف عن مصدر هذه الصفحة، لذا تُرسل طلبات الإعداد بدون جلستك. تحقق من المصدر العام والمنفذ اللذين تم تكوين هذا المثيل بهما، ثم أعد التحميل." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "تمت كتابة الرمز بواسطة واجهة برمجة التطبيقات (API) في سجلها عند بدء التشغيل لأول مرة. إذا قمت بتعيين FLUXER_SETUP_TOKEN، فاستخدم هذه القيمة." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "تعطل التطبيق" @@ -33685,6 +33700,11 @@ msgstr "منح النظام {target} دور {role}" msgid "The system removed the role {role} from {target}" msgstr "أزال النظام دور {role} من {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "يُثبت الرمز المميز أنك تدير هذه النسخة. فقط الشخص الذي يمتلكه يمكنه إنشاء حساب المسؤول." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "جهة خارجية" msgid "This account can't be blocked." msgstr "لا يمكن حظر هذا الحساب." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "هذا الحساب ليس مسؤولاً عن هذا المثيل. سجّل الدخول باستخدام حساب مسؤول لإنهاء الإعداد." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json b/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json index 33e7c6834..08781683e 100644 --- a/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json +++ b/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json @@ -956,6 +956,7 @@ "Enter this code in your browser", "Enter this code in your old {PRODUCT_NAME} app to complete sign-in.", "Enter your full legal name to send the report.", + "Enter your setup token", "Enter your username and the recovery key from your recovery kit, then choose a new password.", "Enter your username and this recovery key.", "Entire screen", @@ -1241,7 +1242,7 @@ "Pin or unpin any message.", "Pin post", "Pinned", - "Plans, perks, gifts and your subscription all live on one page.", + "Plans, perks, gifts and your subscription are all on one page.", "Pop out user", "Pop out video", "Popped out", @@ -1399,6 +1400,7 @@ "Set the default reaction to {emoji}", "Set up", "Set up {productName}", + "Setup token", "Share full-length videos and big files without shrinking them first. Free accounts can send up to {freeSize}.", "Share screen", "Share stream audio", @@ -1508,6 +1510,7 @@ "That code is not valid on {instanceHost}. Check the code in the {productName} app and try again.", "That doesn't look like an instance address. Try a domain such as {exampleInstanceDomain}.", "That image could not be used. Try a different file.", + "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value.", "The camera preview is still stopping. Try streaming again in a moment.", "The connection is encrypted from the start. Usually port 465.", "The connection is never encrypted. Only for a mail server on a network you trust.", @@ -1520,6 +1523,7 @@ "The report form changed. Please start again.", "The sign-in request expired before your browser approved it.", "The sign-in was cancelled in your browser.", + "The token proves that you run this instance. Only the person who has it can create the admin account.", "The update is still downloading. Try again in a few minutes.", "The {PRODUCT_NAME} app asked to sign in as {requestedIdentifier}, but you chose {name}.", "The {PRODUCT_NAME} app on your other device shows it under Signing in on another device. Only enter a code from a device you control.", @@ -1529,6 +1533,7 @@ "There are no recent messages in this thread.", "There are no threads", "These options change how people use the instance. Some can only be chosen during setup.", + "This account is not an admin of this instance. Sign in with an admin account to finish setup.", "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}.", "This animation is too long or too large to crop. Try a shorter or smaller file.", "This browser can't crop animated {avifFormatLabel} files. Upload a GIF or WebP instead.", @@ -1807,9 +1812,9 @@ "{productName} has moved to {host}. Install it from your browser, then sign in with this app.", "{productName} has moved to {host}. Install the new app and you will already be signed in.", "{productName} has moved to {host}. Open it in your browser to install the new app.", + "{productName} is now at {host}. Install it from there, then sign in with this app.", + "{productName} is now at {host}. Install it from there.", "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app.", - "{productName} now lives at {host}. Install it from there, then sign in with this app.", - "{productName} now lives at {host}. Install it from there.", "{productName} recovery kit", "{productName} registered its shortcuts with Hyprland. Bind them in your Hyprland config to use them while {productName} is not focused.", "{productName} wordmark", diff --git a/fluxer_app/src/features/i18n/locales/bg/messages.po b/fluxer_app/src/features/i18n/locales/bg/messages.po index b1275db78..0b23f7bf5 100644 --- a/fluxer_app/src/features/i18n/locales/bg/messages.po +++ b/fluxer_app/src/features/i18n/locales/bg/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} се премести на {host}. Инсталирай н msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} се премести на {host}. Отвори го в браузъра си, за да инсталираш новото приложение." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} вече е на адрес {host}. Инсталирайте го оттам, след което влезте с това приложение." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} вече е на адрес {host}. Инсталирайте го оттам." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} се нуждае от достъп до микрофон msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} се нуждае от достъп до записа на екрана. Разреши записа на екрана в настройките за поверителност на операционната система и рестартирай приложението." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} вече се намира на {host}. Инсталирай го оттам, след което влез с това приложение." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} вече се намира на {host}. Инсталирай го оттам." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Активен разговор" msgid "Call confirmation" msgstr "Потвърждение за обаждане" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Разговорът е в отделен прозорец" @@ -14361,6 +14361,11 @@ msgstr "Въведи името си" msgid "Enter your password." msgstr "Въведи паролата си." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Въведете вашия токен за настройка" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Въведете вашето потребителско име и ключ за възстановяване от комплекта си за възстановяване, след което изберете нова парола." @@ -19448,7 +19453,7 @@ msgstr "Стара цена" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Нека /tts прочете съобщението ти на глас. Изключването на настройката запазва тези команди като обикновен текст." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Позволяване на ботовете от тази общност да ти изпращат DM" @@ -25279,8 +25284,8 @@ msgstr "План" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Планове, екстри, подаръци и абонаментът ви са на една страница." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Планове, предимства, подаръци и абонаментът ви са на една страница." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Отвори потребителя в отделен прозорец" msgid "Pop out video" msgstr "Отвори видеото в отделен прозорец" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "В отделен прозорец" @@ -30120,6 +30125,11 @@ msgstr "Настройки." msgid "Setup key" msgstr "Ключ за настройка" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Токен за настройка" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Тагът #0000 е запазен за абонати на Visionary. V msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API е на различен домейн от тази страница, така че заявките за настройка се изпращат без твоята сесия. Провери публичния домейн и порт, с които е конфигурирана тази инстанция, след което презареди." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API записа токена в своя лог при първото стартиране. Ако сте задали FLUXER_SETUP_TOKEN, използвайте тази стойност." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Приложението се срина" @@ -33685,6 +33700,11 @@ msgstr "Системата даде на {target} ролята {role}" msgid "The system removed the role {role} from {target}" msgstr "Системата премахна ролята {role} от {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Токенът доказва, че управлявате този екземпляр. Само човекът, който го притежава, може да създаде администраторския акаунт." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Трети страни" msgid "This account can't be blocked." msgstr "Този акаунт не може да бъде блокиран." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Този акаунт не е администратор на този инстанс. Влезте с администраторски акаунт, за да завършите настройката." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/cs/messages.po b/fluxer_app/src/features/i18n/locales/cs/messages.po index 82fdf9be8..3b69024a6 100644 --- a/fluxer_app/src/features/i18n/locales/cs/messages.po +++ b/fluxer_app/src/features/i18n/locales/cs/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} se přesunul na {host}. Nainstalujte novou aplikaci a bude msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} se přesunul na {host}. Otevřete jej v prohlížeči a nainstalujte novou aplikaci." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} je teď na {host}. Nainstalujte si ho odtamtud a pak se přihlaste pomocí této aplikace." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} je teď na {host}. Nainstalujte si ho odtud." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "Aplikace {productName} potřebuje přístup k mikrofonu, aby mohla zobra msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} potřebuje přístup k nahrávání obrazovky. Povolte nahrávání obrazovky v nastavení ochrany osobních údajů vašeho operačního systému a restartujte aplikaci." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} se nyní nachází na adrese {host}. Nainstalujte si ho odtud a poté se přihlaste pomocí této aplikace." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} se teď nachází na adrese {host}. Nainstalujte si ho odtud." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Hovor k dispozici" msgid "Call confirmation" msgstr "Potvrzení hovoru" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Hovor je v samostatném okně" @@ -14361,6 +14361,11 @@ msgstr "Zadejte své jméno" msgid "Enter your password." msgstr "Zadejte heslo." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Zadejte svůj nastavovací token" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Zadejte své uživatelské jméno a obnovovací klíč z vaší obnovovací sady a poté zvolte nové heslo." @@ -19448,7 +19453,7 @@ msgstr "Původní cena" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Nechte /tts přečíst vaši zprávu nahlas. Vypnutím tohoto nastavení zůstanou tyto příkazy jako běžný text." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Povolit botům v této komunitě posílat vám přímé zprávy" @@ -25279,8 +25284,8 @@ msgstr "Plán" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Plány, výhody, dárky a vaše předplatné – vše najdete na jedné stránce." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Plány, výhody, dárky a vaše předplatné – vše na jedné stránce." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Otevřít uživatele v samostatném okně" msgid "Pop out video" msgstr "Zobrazit video v samostatném okně" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "V samostatném okně" @@ -30120,6 +30125,11 @@ msgstr "Nastavení." msgid "Setup key" msgstr "Nastavovací klíč" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Nastavovací token" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Značka #0000 je vyhrazena předplatitelům Visionary. Visionary již ne msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API má jiný origin než tato stránka, takže požadavky na nastavení se odesílají bez vaší relace. Zkontrolujte veřejný origin a port nastavený pro tuto instanci a poté stránku znovu načtěte." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "Token se poprvé zapsal do logu API při jeho spuštění. Pokud jste nastavili FLUXER_SETUP_TOKEN, použijte tuto hodnotu." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Aplikace spadla" @@ -33685,6 +33700,11 @@ msgstr "Systém udělil roli {role} uživateli {target}" msgid "The system removed the role {role} from {target}" msgstr "Systém odebral roli {role} uživateli {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Token potvrzuje, že spravujete tuto instanci. Pouze osoba, která ho má, může vytvořit účet správce." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Třetí strana" msgid "This account can't be blocked." msgstr "Tento účet nelze zablokovat." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Tento účet není administrátorem této instance. Pro dokončení nastavení se přihlaste pomocí účtu administrátora." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/da/messages.po b/fluxer_app/src/features/i18n/locales/da/messages.po index c04ba72e5..4d1252e2d 100644 --- a/fluxer_app/src/features/i18n/locales/da/messages.po +++ b/fluxer_app/src/features/i18n/locales/da/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} er flyttet til {host}. Installer den nye app, og du vil al msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} er flyttet til {host}. Åbn det i din browser for at installere den nye app." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} er nu på {host}. Installer det derfra, og log derefter ind med denne app." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} er nu på {host}. Installer det derfra." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} skal have adgang til mikrofonen for at vise dine enheder." msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} skal have adgang til skærmoptagelse. Tillad skærmoptagelse i operativsystemets privatlivsindstillinger, og genstart appen." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} findes nu på {host}. Installer det derfra, og log derefter ind med denne app." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} findes nu på {host}. Installer det derfra." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Opkald tilgængeligt" msgid "Call confirmation" msgstr "Opkaldsbekræftelse" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Opkaldet er åbnet i et separat vindue" @@ -14361,6 +14361,11 @@ msgstr "Indtast dit navn" msgid "Enter your password." msgstr "Indtast din adgangskode." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Indtast din opsætningstoken" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Indtast dit brugernavn og den gendannelsesnøgle, du fik fra dit gendannelsessæt, og vælg derefter en ny adgangskode." @@ -19448,7 +19453,7 @@ msgstr "Gammel pris" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Lad /tts læse din besked højt. Hvis du deaktiverer indstillingen, forbliver disse kommandoer som almindelig tekst." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Lad bots i dette fællesskab sende dig direkte beskeder" @@ -25279,8 +25284,8 @@ msgstr "Abonnement" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Abonnementer, fordele, gaver og dit abonnement findes alt sammen på én side." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Abonnementer, fordele, gaver og dit abonnement er samlet på én side." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Vis brugeren i et separat vindue" msgid "Pop out video" msgstr "Vis video i separat vindue" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Åbnet i separat vindue" @@ -30120,6 +30125,11 @@ msgstr "Indstillinger." msgid "Setup key" msgstr "Opsætningsnøgle" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Opsætningstoken" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Tagget #0000 er reserveret til Visionary-abonnenter. Visionary kan ikke msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API’en har en anden kombination af protokol, vært og port end denne side, så opsætningsanmodninger sendes uden din session. Kontroller den offentlige adresse og port, som instansen er konfigureret med, og genindlæs derefter." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API'en skrev tokenet til sin log første gang, den startede. Hvis du har angivet FLUXER_SETUP_TOKEN, skal du bruge den værdi." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Appen gik ned" @@ -33685,6 +33700,11 @@ msgstr "Systemet gav {target} rollen {role}" msgid "The system removed the role {role} from {target}" msgstr "Systemet fjernede rollen {role} fra {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Tokenet beviser, at du kører denne instans. Kun den person, der har den, kan oprette administratorkontoen." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Tredjepart" msgid "This account can't be blocked." msgstr "Denne konto kan ikke blokeres." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Denne konto er ikke administrator for denne instans. Log ind med en administrator-konto for at afslutte opsætningen." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/de/messages.po b/fluxer_app/src/features/i18n/locales/de/messages.po index 7dbadf92a..6a58a846f 100644 --- a/fluxer_app/src/features/i18n/locales/de/messages.po +++ b/fluxer_app/src/features/i18n/locales/de/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} ist zu {host} umgezogen. Installiere die neue App und du b msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} ist zu {host} umgezogen. Öffne es in deinem Browser, um die neue App zu installieren." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} ist jetzt unter {host} erreichbar. Installiere es von dort und melde dich dann mit dieser App an." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} ist jetzt unter {host} erreichbar. Installiere es von dort." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} benötigt Mikrofonzugriff, um deine Geräte aufzulisten." msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} benötigt Zugriff auf die Bildschirmaufnahme. Erlaube die Bildschirmaufnahme in den Datenschutzeinstellungen deines Betriebssystems und starte die App neu." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} ist jetzt unter {host} erreichbar. Installiere es von dort und melde dich dann mit dieser App an." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} ist jetzt unter {host} erreichbar. Installiere es von dort." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Anruf verfügbar" msgid "Call confirmation" msgstr "Anrufbestätigung" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Anruf ist in einem separaten Fenster geöffnet" @@ -14361,6 +14361,11 @@ msgstr "Gib deinen Namen ein" msgid "Enter your password." msgstr "Gib dein Passwort ein." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Gib dein Einrichtungstoken ein" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Gib deinen Benutzernamen und den Wiederherstellungsschlüssel aus deinem Wiederherstellungskit ein und wähle dann ein neues Passwort." @@ -19448,7 +19453,7 @@ msgstr "Alter Tarif" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Lass /tts deine Nachricht vorlesen. Wenn du die Einstellung deaktivierst, werden diese Befehle als normaler Text angezeigt." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Bots in dieser Community dürfen dir DMs senden" @@ -25279,7 +25284,7 @@ msgstr "Tarif" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "Pläne, Vorteile, Geschenke und dein Abonnement – alles auf einer Seite." #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "Nutzer auslagern" msgid "Pop out video" msgstr "Video auslagern" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "In separatem Fenster geöffnet" @@ -30120,6 +30125,11 @@ msgstr "Einstellungen." msgid "Setup key" msgstr "Einrichtungsschlüssel" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Einrichtungstoken" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Der Tag #0000 ist für Visionary-Abonnenten reserviert. Visionary kann n msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "Die API hat einen anderen Ursprung als diese Seite, daher werden Setup-Anfragen ohne deine Sitzung gesendet. Überprüfe den öffentlichen Ursprung und Port, mit dem diese Instanz konfiguriert ist, und lade dann neu." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "Die API hat das Token beim ersten Start in ihr Protokoll geschrieben. Wenn du FLUXER_SETUP_TOKEN gesetzt hast, verwende diesen Wert." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Die App ist abgestürzt" @@ -33685,6 +33700,11 @@ msgstr "Das System hat {target} die Rolle {role} gegeben" msgid "The system removed the role {role} from {target}" msgstr "Das System hat die Rolle {role} von {target} entfernt" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Das Token beweist, dass du diese Instanz betreibst. Nur die Person, die es hat, kann den Admin-Account erstellen." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Drittanbieter" msgid "This account can't be blocked." msgstr "Dieser Account kann nicht blockiert werden." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Dieses Konto ist kein Administrator dieser Instanz. Melde dich mit einem Administratorkonto an, um die Einrichtung abzuschließen." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/el/messages.po b/fluxer_app/src/features/i18n/locales/el/messages.po index 226ecd366..19203c5a1 100644 --- a/fluxer_app/src/features/i18n/locales/el/messages.po +++ b/fluxer_app/src/features/i18n/locales/el/messages.po @@ -1911,6 +1911,16 @@ msgstr "Το {productName} μεταφέρθηκε στο {host}. Εγκατασ msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "Το {productName} μεταφέρθηκε στο {host}. Ανοίξτε το στο πρόγραμμα περιήγησής σας για να εγκαταστήσετε τη νέα εφαρμογή." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "Το {productName} βρίσκεται πλέον στο {host}. Εγκαταστήστε το από εκεί και, στη συνέχεια, συνδεθείτε με αυτήν την εφαρμογή." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "Το {productName} βρίσκεται τώρα στο {host}. Εγκαταστήστε το από εκεί." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "Το {productName} χρειάζεται πρόσβαση στο μικρ msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "Το {productName} χρειάζεται πρόσβαση στην εγγραφή οθόνης. Επιτρέψτε την εγγραφή οθόνης στις ρυθμίσεις απορρήτου του λειτουργικού σας συστήματος και επανεκκινήστε την εφαρμογή." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "Το {productName} βρίσκεται πλέον στο {host}. Εγκαταστήστε το από εκεί και, στη συνέχεια, συνδεθείτε με αυτήν την εφαρμογή." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "Το {productName} βρίσκεται πλέον στο {host}. Εγκαταστήστε το από εκεί." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Διαθέσιμη κλήση" msgid "Call confirmation" msgstr "Επιβεβαίωση κλήσης" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Η κλήση είναι σε ξεχωριστό παράθυρο" @@ -14361,6 +14361,11 @@ msgstr "Πληκτρολογήστε το όνομά σας" msgid "Enter your password." msgstr "Εισάγετε τον κωδικό πρόσβασής σας." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Εισαγάγετε το διακριτικό ρύθμισης" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Εισαγάγετε το όνομα χρήστη σας και το κλειδί ανάκτησης από το κιτ ανάκτησής σας, και στη συνέχεια επιλέξτε έναν νέο κωδικό πρόσβασης." @@ -19448,7 +19453,7 @@ msgstr "Παλιά τιμή" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Αφήστε το /tts να διαβάσει το μήνυμά σας δυνατά. Η απενεργοποίηση της ρύθμισης διατηρεί αυτές τις εντολές ως κανονικό κείμενο." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Να επιτρέπονται τα DM από bot αυτής της κοινότητας" @@ -25279,7 +25284,7 @@ msgstr "Πρόγραμμα" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "Πλάνα, προνόμια, δώρα και η συνδρομή σας βρίσκονται όλα σε μία σελίδα." #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "Άνοιγμα χρήστη σε ξεχωριστό παράθυρο" msgid "Pop out video" msgstr "Άνοιγμα βίντεο σε ξεχωριστό παράθυρο" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Εμφανίζεται σε ξεχωριστό παράθυρο" @@ -30120,6 +30125,11 @@ msgstr "Ρυθμίσεις." msgid "Setup key" msgstr "Κλειδί εγκατάστασης" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Διακριτικό ρύθμισης" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Η ετικέτα #0000 είναι δεσμευμένη για τους msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "Το API βρίσκεται σε διαφορετική προέλευση από αυτήν τη σελίδα, οπότε τα αιτήματα ρύθμισης αποστέλλονται χωρίς τη συνεδρία σας. Ελέγξτε τη δημόσια προέλευση και τη θύρα με την οποία έχει ρυθμιστεί αυτή η παρουσία και έπειτα ανανεώστε τη σελίδα." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "Το API έγραψε το token στο αρχείο καταγραφής του κατά την πρώτη εκκίνηση. Αν ορίσατε το FLUXER_SETUP_TOKEN, χρησιμοποιήστε αυτήν την τιμή." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Η εφαρμογή έκλεισε απροσδόκητα" @@ -33685,6 +33700,11 @@ msgstr "Το σύστημα έδωσε στον/στην {target} τον ρόλ msgid "The system removed the role {role} from {target}" msgstr "Το σύστημα αφαίρεσε τον ρόλο {role} από τον/την {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Το token αποδεικνύει ότι διαχειρίζεστε αυτήν την παρουσία. Μόνο το άτομο που το έχει μπορεί να δημιουργήσει τον λογαριασμό διαχειριστή." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Τρίτα μέρη" msgid "This account can't be blocked." msgstr "Αυτός ο λογαριασμός δεν μπορεί να μπλοκαριστεί." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Αυτός ο λογαριασμός δεν είναι διαχειριστής αυτής της παρουσίας. Συνδεθείτε με έναν λογαριασμό διαχειριστή για να ολοκληρώσετε τη ρύθμιση." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/en-GB/messages.po b/fluxer_app/src/features/i18n/locales/en-GB/messages.po index 674446221..d9d12e51b 100644 --- a/fluxer_app/src/features/i18n/locales/en-GB/messages.po +++ b/fluxer_app/src/features/i18n/locales/en-GB/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} has moved to {host}. Install the new app and you will alre msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} has moved to {host}. Open it in your browser to install the new app." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} is now at {host}. Install it from there, then sign in with this app." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} is now at {host}. Install it from there." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} needs microphone access to list your devices." msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} now lives at {host}. Install it from there, then sign in with this app." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} now lives at {host}. Install it from there." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Call available" msgid "Call confirmation" msgstr "Call confirmation" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Call is popped out" @@ -14361,6 +14361,11 @@ msgstr "Enter your name" msgid "Enter your password." msgstr "Enter your password." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Enter your setup token" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Enter your username and the recovery key from your recovery kit, then choose a new password." @@ -19448,7 +19453,7 @@ msgstr "Legacy rate" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Let bots in this community DM you" @@ -25279,8 +25284,8 @@ msgstr "Plan" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Plans, perks, gifts and your subscription are all on one page." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Pop out user" msgid "Pop out video" msgstr "Pop out video" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Popped out" @@ -30120,6 +30125,11 @@ msgstr "Settings." msgid "Setup key" msgstr "Setup key" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Setup token" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "The #0000 tag is reserved for Visionary subscribers. Visionary is no lon msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "The app crashed" @@ -33685,6 +33700,11 @@ msgstr "The system gave {target} the role {role}" msgid "The system removed the role {role} from {target}" msgstr "The system removed the role {role} from {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "The token proves that you run this instance. Only the person who has it can create the admin account." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Third party" msgid "This account can't be blocked." msgstr "This account can't be blocked." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "This account is not an admin of this instance. Sign in with an admin account to finish setup." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/en-US/messages.po b/fluxer_app/src/features/i18n/locales/en-US/messages.po index 0c839e71f..2da5bb6f8 100644 --- a/fluxer_app/src/features/i18n/locales/en-US/messages.po +++ b/fluxer_app/src/features/i18n/locales/en-US/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} has moved to {host}. Install the new app and you will alre msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} has moved to {host}. Open it in your browser to install the new app." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} is now at {host}. Install it from there, then sign in with this app." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} is now at {host}. Install it from there." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} needs microphone access to list your devices." msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} now lives at {host}. Install it from there, then sign in with this app." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} now lives at {host}. Install it from there." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Call available" msgid "Call confirmation" msgstr "Call confirmation" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Call is popped out" @@ -14361,6 +14361,11 @@ msgstr "Enter your name" msgid "Enter your password." msgstr "Enter your password." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Enter your setup token" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Enter your username and the recovery key from your recovery kit, then choose a new password." @@ -19448,7 +19453,7 @@ msgstr "Legacy rate" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Let bots in this community DM you" @@ -25279,8 +25284,8 @@ msgstr "Plan" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Plans, perks, gifts and your subscription are all on one page." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Pop out user" msgid "Pop out video" msgstr "Pop out video" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Popped out" @@ -30120,6 +30125,11 @@ msgstr "Settings." msgid "Setup key" msgstr "Setup key" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Setup token" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "The #0000 tag is reserved for Visionary subscribers. Visionary is no lon msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "The app crashed" @@ -33685,6 +33700,11 @@ msgstr "The system gave {target} the role {role}" msgid "The system removed the role {role} from {target}" msgstr "The system removed the role {role} from {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "The token proves that you run this instance. Only the person who has it can create the admin account." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Third party" msgid "This account can't be blocked." msgstr "This account can't be blocked." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "This account is not an admin of this instance. Sign in with an admin account to finish setup." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/es-419/messages.po b/fluxer_app/src/features/i18n/locales/es-419/messages.po index 4d90b61d3..84e3be9fd 100644 --- a/fluxer_app/src/features/i18n/locales/es-419/messages.po +++ b/fluxer_app/src/features/i18n/locales/es-419/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} se mudó a {host}. Instala la nueva app y ya habrás inici msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} se mudó a {host}. Ábrelo en tu navegador para instalar la nueva app." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} ahora está en {host}. Instálalo desde allí y luego inicia sesión con esta app." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} ahora está en {host}. Instálalo desde allí." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} necesita acceso al micrófono para mostrar tus dispositivo msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} necesita acceso a la grabación de pantalla. Permite la grabación de pantalla en la configuración de privacidad de tu sistema operativo y reinicia la aplicación." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} ahora está en {host}. Instálalo desde allí y luego inicia sesión con esta app." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} ahora está en {host}. Instálalo desde allí." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Llamada disponible" msgid "Call confirmation" msgstr "Confirmación de llamada" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "La llamada está en una ventana separada" @@ -14361,6 +14361,11 @@ msgstr "Ingresa tu nombre" msgid "Enter your password." msgstr "Ingresa tu contraseña." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Ingresa tu token de configuración" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Ingresa tu nombre de usuario y la clave de recuperación de tu kit de recuperación, luego elige una nueva contraseña." @@ -19448,7 +19453,7 @@ msgstr "Tarifa anterior" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Permite que /tts lea tu mensaje en voz alta. Si desactivas esta opción, esos comandos se mantendrán como texto normal." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Permitir que los bots de esta comunidad te envíen MD" @@ -25279,7 +25284,7 @@ msgstr "Plan" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "Planes, beneficios, regalos y tu suscripción, todo en una sola página." #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "Abrir usuario en ventana aparte" msgid "Pop out video" msgstr "Abrir video en ventana aparte" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "En ventana separada" @@ -30120,6 +30125,11 @@ msgstr "Configuración." msgid "Setup key" msgstr "Clave de configuración" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Token de configuración" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "La etiqueta #0000 está reservada para suscriptores Visionary. Visionary msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "La API está en un origen diferente al de esta página, por lo que las solicitudes de configuración se envían sin tu sesión. Verifica el origen público y el puerto con los que está configurada esta instancia, y luego recarga." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "La API escribió el token en su registro la primera vez que se inició. Si configuraste FLUXER_SETUP_TOKEN, usa ese valor." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "La aplicación falló" @@ -33685,6 +33700,11 @@ msgstr "El sistema le dio el rol de {role} a {target}" msgid "The system removed the role {role} from {target}" msgstr "El sistema eliminó el rol {role} de {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "El token demuestra que ejecutas esta instancia. Solo la persona que lo tiene puede crear la cuenta de administrador." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Terceros" msgid "This account can't be blocked." msgstr "No se puede bloquear esta cuenta." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Esta cuenta no es administradora de esta instancia. Inicia sesión con una cuenta de administrador para finalizar la configuración." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/es-ES/messages.po b/fluxer_app/src/features/i18n/locales/es-ES/messages.po index e1ee9b1f8..9b4baa735 100644 --- a/fluxer_app/src/features/i18n/locales/es-ES/messages.po +++ b/fluxer_app/src/features/i18n/locales/es-ES/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} se ha mudado a {host}. Instala la nueva app y ya habrás i msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} se ha mudado a {host}. Ábrelo en tu navegador para instalar la nueva app." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} ahora está en {host}. Instálalo desde allí y luego inicia sesión con esta aplicación." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} ahora está en {host}. Instálalo desde allí." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} necesita acceso al micrófono para mostrar tus dispositivo msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} necesita acceso a la grabación de pantalla. Permite la grabación de pantalla en los ajustes de privacidad de tu sistema operativo y reinicia la aplicación." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} ahora está en {host}. Instálalo desde allí e inicia sesión con esta aplicación." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} ahora está en {host}. Instálalo desde allí." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Llamada disponible" msgid "Call confirmation" msgstr "Confirmación de llamada" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "La llamada está en una ventana separada" @@ -14361,6 +14361,11 @@ msgstr "Introduce tu nombre" msgid "Enter your password." msgstr "Introduce tu contraseña." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Introduce tu token de configuración" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Introduce tu nombre de usuario y la clave de recuperación de tu kit, luego elige una nueva contraseña." @@ -19448,7 +19453,7 @@ msgstr "Tarifa antigua" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Permite que /tts lea tu mensaje en voz alta. Si desactivas este ajuste, esos comandos se mantendrán como texto normal." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Permitir que los bots de esta comunidad te envíen MD" @@ -25279,8 +25284,8 @@ msgstr "Plan" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Planes, ventajas, regalos y tu suscripción, todo en una sola página." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Planes, ventajas, regalos y tu suscripción, todo en una página." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Abrir usuario en ventana separada" msgid "Pop out video" msgstr "Abrir vídeo en ventana aparte" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "En ventana emergente" @@ -30120,6 +30125,11 @@ msgstr "Ajustes." msgid "Setup key" msgstr "Clave de configuración" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Token de configuración" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "La etiqueta #0000 está reservada para los suscriptores Visionary. Visio msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "La API está en un origen diferente al de esta página, por lo que las solicitudes de configuración se envían sin tu sesión. Comprueba el origen público y el puerto con los que está configurada esta instancia y, a continuación, recarga." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "La API escribió el token en su registro la primera vez que se inició. Si has configurado FLUXER_SETUP_TOKEN, usa ese valor." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "La aplicación se ha bloqueado" @@ -33685,6 +33700,11 @@ msgstr "El sistema otorgó a {target} el rol de {role}" msgid "The system removed the role {role} from {target}" msgstr "El sistema eliminó el rol {role} de {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "El token demuestra que ejecutas esta instancia. Solo la persona que lo tenga podrá crear la cuenta de administrador." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Terceros" msgid "This account can't be blocked." msgstr "No se puede bloquear esta cuenta." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Esta cuenta no es administradora de esta instancia. Inicia sesión con una cuenta de administrador para finalizar la configuración." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/fi/messages.po b/fluxer_app/src/features/i18n/locales/fi/messages.po index 48616eff6..7fe9f3d18 100644 --- a/fluxer_app/src/features/i18n/locales/fi/messages.po +++ b/fluxer_app/src/features/i18n/locales/fi/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} on siirtynyt osoitteeseen {host}. Asenna uusi sovellus, ni msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} on siirtynyt osoitteeseen {host}. Avaa se selaimessasi asentaaksesi uuden sovelluksen." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} on nyt osoitteessa {host}. Asenna se sieltä ja kirjaudu sitten sisään tällä sovelluksella." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} on nyt osoitteessa {host}. Asenna se sieltä." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} tarvitsee mikrofonin käyttöoikeuden, jotta se voi näytt msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} tarvitsee pääsyn näytön tallennukseen. Salli näytön tallennus käyttöjärjestelmäsi tietosuoja-asetuksissa ja käynnistä sovellus uudelleen." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} löytyy nyt osoitteesta {host}. Asenna se sieltä ja kirjaudu sitten sisään tällä sovelluksella." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} löytyy nyt osoitteesta {host}. Asenna se sieltä." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Puhelu käytettävissä" msgid "Call confirmation" msgstr "Puhelun vahvistus" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Puhelu on avattu erilliseen ikkunaan" @@ -14361,6 +14361,11 @@ msgstr "Kirjoita nimesi" msgid "Enter your password." msgstr "Anna salasanasi." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Syötä asennustunnuksesi" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Syötä käyttäjänimesi ja palautusavaimesi palautuspaketistasi ja valitse sitten uusi salasana." @@ -19448,7 +19453,7 @@ msgstr "Vanha hinta" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "/tts lukee viestisi ääneen. Jos asetus poistetaan käytöstä, komennot näkyvät tavallisena tekstinä." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Salli tämän yhteisön bottien lähettää sinulle yksityisviestejä" @@ -25279,8 +25284,8 @@ msgstr "Tilaus" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Suunnitelmat, edut, lahjat ja tilauksesi löytyvät kaikki yhdeltä sivulta." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Suunnitelmat, edut, lahjat ja tilauksesi ovat kaikki yhdellä sivulla." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Siirrä käyttäjä erilliseen ikkunaan" msgid "Pop out video" msgstr "Siirrä video erilliseen ikkunaan" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Erillisessä ikkunassa" @@ -30120,6 +30125,11 @@ msgstr "Asetukset." msgid "Setup key" msgstr "Asetusavain" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Asennustunnus" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Tunniste #0000 on varattu Visionary-tilaajille. Visionary ei ole enää msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API on eri alkuperää kuin tämä sivu, joten asetuspyynnöt lähetetään ilman istuntoasi. Tarkista tämän instanssin julkinen alkuperä ja portti, ja lataa sitten uudelleen." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API kirjoitti tunnuksen lokiinsa ensimmäisellä käynnistyskerralla. Jos olet määrittänyt FLUXER_SETUP_TOKEN, käytä sitä arvoa." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Sovellus kaatui" @@ -33685,6 +33700,11 @@ msgstr "Järjestelmä antoi roolin {role} käyttäjälle {target}" msgid "The system removed the role {role} from {target}" msgstr "Järjestelmä poisti roolin {role} käyttäjältä {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Tunnus todistaa, että käytät tätä instanssia. Vain henkilö, jolla se on, voi luoda pääkäyttäjätilin." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Kolmas osapuoli" msgid "This account can't be blocked." msgstr "Tätä tiliä ei voi estää." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Tämä tili ei ole tämän instanssin ylläpitäjä. Kirjaudu sisään ylläpitäjätilillä viimeistelläksesi asennuksen." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/fr/messages.po b/fluxer_app/src/features/i18n/locales/fr/messages.po index 3fb0356a2..248af09c5 100644 --- a/fluxer_app/src/features/i18n/locales/fr/messages.po +++ b/fluxer_app/src/features/i18n/locales/fr/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} a déménagé sur {host}. Installez la nouvelle applicatio msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} a déménagé sur {host}. Ouvrez-le dans votre navigateur pour installer la nouvelle application." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} est maintenant sur {host}. Installez-le à partir de là, puis connectez-vous avec cette application." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} est maintenant sur {host}. Installez-le à partir de là." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} a besoin d'accéder à votre microphone pour afficher vos msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} a besoin d'accéder à l'enregistrement d'écran. Autorisez l'enregistrement d'écran dans les paramètres de confidentialité de votre système d'exploitation et redémarrez l'application." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} est maintenant disponible sur {host}. Installez-le à partir de là, puis connectez-vous avec cette application." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} est maintenant disponible sur {host}. Installez-le à partir de là." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Appel disponible" msgid "Call confirmation" msgstr "Confirmation d'appel" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "L’appel est dans une fenêtre séparée" @@ -14361,6 +14361,11 @@ msgstr "Saisissez votre nom" msgid "Enter your password." msgstr "Saisissez votre mot de passe." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Entrez votre jeton d'installation" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Entrez votre nom d'utilisateur et la clé de récupération de votre kit de récupération, puis choisissez un nouveau mot de passe." @@ -19448,7 +19453,7 @@ msgstr "Ancien tarif" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Laissez /tts lire votre message à voix haute. Désactiver ce paramètre permet de conserver ces commandes comme du texte normal." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Autoriser les bots de cette communauté à vous envoyer des messages privés" @@ -25279,8 +25284,8 @@ msgstr "Forfait" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Abonnements, avantages, cadeaux et votre souscription sont regroupés sur une seule page." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Vos forfaits, avantages, cadeaux et votre abonnement sont sur une seule page." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Afficher l’utilisateur dans une fenêtre séparée" msgid "Pop out video" msgstr "Afficher la vidéo dans une fenêtre séparée" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Affiché dans une fenêtre séparée" @@ -30120,6 +30125,11 @@ msgstr "Paramètres." msgid "Setup key" msgstr "Clé de configuration" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Jeton d'installation" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Le tag #0000 est réservé aux abonnés Visionary. L'abonnement Visionar msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "L'API est sur une origine différente de celle de cette page, les requêtes de configuration sont donc envoyées sans votre session. Vérifiez l'origine publique et le port configurés pour cette instance, puis rechargez." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "L'API a écrit le jeton dans son journal lors de son premier démarrage. Si vous avez défini FLUXER_SETUP_TOKEN, utilisez cette valeur." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "L'application a planté" @@ -33685,6 +33700,11 @@ msgstr "Le système a donné le rôle {role} à {target}" msgid "The system removed the role {role} from {target}" msgstr "Le système a retiré le rôle {role} de {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Le jeton prouve que vous gérez cette instance. Seule la personne qui le possède peut créer le compte administrateur." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Tiers" msgid "This account can't be blocked." msgstr "Ce compte ne peut pas être bloqué." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Ce compte n'est pas administrateur de cette instance. Connectez-vous avec un compte administrateur pour terminer la configuration." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/he/messages.po b/fluxer_app/src/features/i18n/locales/he/messages.po index 6536adb7e..5af80e088 100644 --- a/fluxer_app/src/features/i18n/locales/he/messages.po +++ b/fluxer_app/src/features/i18n/locales/he/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} עבר ל-{host}. התקן את האפליקציה החד msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} עבר ל-{host}. פתח אותו בדפדפן כדי להתקין את האפליקציה החדשה." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} נמצא כעת בכתובת {host}. התקן אותו משם, ולאחר מכן היכנס באמצעות האפליקציה הזו." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} נמצא כעת בכתובת {host}. התקן משם." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} זקוק לגישה למיקרופון כדי להציג msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} זקוק לגישה להקלטת מסך. אפשר הקלטת מסך בהגדרות הפרטיות של מערכת ההפעלה שלך והפעל מחדש את האפליקציה." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} נמצא כעת בכתובת {host}. התקן אותו משם, ולאחר מכן התחבר באמצעות האפליקציה הזו." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} נמצא כעת בכתובת {host}. התקן אותו משם." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "שיחה זמינה" msgid "Call confirmation" msgstr "אישור שיחה" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "השיחה הועברה לחלון נפרד" @@ -14361,6 +14361,11 @@ msgstr "הזן את שמך" msgid "Enter your password." msgstr "הזן את הסיסמה שלך." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "הזן את אסימון ההגדרה שלך" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "הזן את שם המשתמש שלך ואת מפתח השחזור מתוך ערכת השחזור שלך, ואז בחר סיסמה חדשה." @@ -19448,7 +19453,7 @@ msgstr "תעריף קודם" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "אפשר ל-/tts להקריא את ההודעה שלך בקול. השבתת ההגדרה משאירה את הפקודות האלה כטקסט רגיל." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "אפשר לבוטים בקהילה זו לשלוח לך הודעות פרטיות" @@ -25279,7 +25284,7 @@ msgstr "מסלול" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "תוכניות, הטבות, מתנות והמנוי שלך נמצאים כולם בדף אחד." #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "הצג משתמש בחלון נפרד" msgid "Pop out video" msgstr "הצג וידאו בחלון נפרד" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "הועבר לחלון נפרד" @@ -30120,6 +30125,11 @@ msgstr "הגדרות." msgid "Setup key" msgstr "מפתח הגדרה" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "אסימון הגדרה" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "התגית #0000 שמורה למנויי Visionary. מינוי Visionary msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "ה-API נמצא במקור שונה מהדף הזה, לכן בקשות ההגדרה נשלחות ללא הסשן שלך. בדוק את המקור והפורט הציבוריים שהמופע הזה מוגדר איתם, ואז טען מחדש." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "ה-API כתב את האסימון ללוג שלו בפעם הראשונה שהוא הופעל. אם הגדרת את FLUXER_SETUP_TOKEN, השתמש בערך זה." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "האפליקציה קרסה" @@ -33685,6 +33700,11 @@ msgstr "המערכת נתנה ל-{target} את התפקיד {role}" msgid "The system removed the role {role} from {target}" msgstr "המערכת הסירה את התפקיד {role} מ-{target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "הטוקן מוכיח שאתה מנהל את המופע הזה. רק מי שיש לו אותו יכול ליצור את חשבון המנהל." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "צד שלישי" msgid "This account can't be blocked." msgstr "לא ניתן לחסום חשבון זה." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "החשבון הזה אינו מנהל של המופע הזה. היכנס עם חשבון מנהל כדי לסיים את ההגדרה." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/hi/messages.po b/fluxer_app/src/features/i18n/locales/hi/messages.po index 05db5cc63..47ab8658a 100644 --- a/fluxer_app/src/features/i18n/locales/hi/messages.po +++ b/fluxer_app/src/features/i18n/locales/hi/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} {host} पर चला गया है। नया ऐ msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} {host} पर चला गया है। नया ऐप इंस्टॉल करने के लिए इसे अपने ब्राउज़र में खोलें।" +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} अब {host} पर है। वहां से इसे इंस्टॉल करें, फिर इस ऐप से साइन इन करें।" + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} अब {host} पर है। वहां से इंस्टॉल करें।" + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} को आपके डिवाइस दिखाने msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} को स्क्रीन रिकॉर्डिंग का एक्सेस चाहिए। अपने ऑपरेटिंग सिस्टम की गोपनीयता सेटिंग्स में स्क्रीन रिकॉर्डिंग की अनुमति दें और ऐप को रीस्टार्ट करें।" -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} अब {host} पर उपलब्ध है। वहां से इसे इंस्टॉल करें, फिर इस ऐप से साइन इन करें।" - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} अब {host} पर उपलब्ध है। वहां से इसे इंस्टॉल करें।" - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "कॉल उपलब्ध है" msgid "Call confirmation" msgstr "कॉल की पुष्टि" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "कॉल पॉप आउट है" @@ -14361,6 +14361,11 @@ msgstr "अपना नाम डालें" msgid "Enter your password." msgstr "अपना पासवर्ड डालें।" +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "अपना सेटअप टोकन दर्ज करें" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "अपना यूज़रनेम और अपनी रिकवरी किट से रिकवरी की दर्ज करें, फिर एक नया पासवर्ड चुनें।" @@ -19448,7 +19453,7 @@ msgstr "पुरानी दर" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "/tts को आपका मैसेज ज़ोर से पढ़ने दें। सेटिंग बंद करने पर ये कमांड सामान्य टेक्स्ट के रूप में दिखेंगे।" -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "इस कम्युनिटी में मौजूद बॉट को आपको DM करने दें" @@ -25279,8 +25284,8 @@ msgstr "प्लान" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "योजनाएं, लाभ, उपहार और आपकी सदस्यता - सब एक ही पेज पर उपलब्ध हैं।" +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "योजनाएं, लाभ, उपहार और आपकी सदस्यता - सब एक ही पेज पर हैं।" #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "यूज़र को पॉप आउट करें" msgid "Pop out video" msgstr "वीडियो पॉप आउट करें" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "अलग विंडो में" @@ -30120,6 +30125,11 @@ msgstr "सेटिंग्स।" msgid "Setup key" msgstr "सेटअप कुंजी" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "सेटअप टोकन" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "#0000 टैग विजनरी सब्सक्राइबर क msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API इस पेज से अलग ओरिजिन पर है, इसलिए सेटअप अनुरोध आपके सेशन के बिना भेजे जा रहे हैं। इस इंस्टेंस के लिए कॉन्फ़िगर किए गए पब्लिक ओरिजिन और पोर्ट की जाँच करें, फिर रीलोड करें।" +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API ने पहली बार शुरू होने पर टोकन को अपने लॉग में लिख दिया था। यदि आपने FLUXER_SETUP_TOKEN सेट किया है, तो उस मान का उपयोग करें।" + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "ऐप क्रैश हो गया" @@ -33685,6 +33700,11 @@ msgstr "सिस्टम ने {target} को {role} रोल दिया" msgid "The system removed the role {role} from {target}" msgstr "सिस्टम ने {role} रोल {target} से हटा दिया" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "यह टोकन साबित करता है कि आप यह इंस्टेंस चला रहे हैं। केवल वही व्यक्ति एडमिन खाता बना सकता है जिसके पास यह है।" + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "थर्ड पार्टी" msgid "This account can't be blocked." msgstr "इस अकाउंट को ब्लॉक नहीं किया जा सकता।" +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "यह खाता इस इंस्टेंस का एडमिन नहीं है। सेटअप पूरा करने के लिए एडमिन खाते से साइन इन करें।" + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/hr/messages.po b/fluxer_app/src/features/i18n/locales/hr/messages.po index 671ad65f4..d20b04719 100644 --- a/fluxer_app/src/features/i18n/locales/hr/messages.po +++ b/fluxer_app/src/features/i18n/locales/hr/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} se preselio na {host}. Instalirajte novu aplikaciju i već msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} se preselio na {host}. Otvorite ga u pregledniku kako biste instalirali novu aplikaciju." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} je sada na adresi {host}. Instalirajte ga od tamo, a zatim se prijavite ovom aplikacijom." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} je sada na adresi {host}. Instalirajte ga od tamo." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "Aplikacija {productName} treba pristup mikrofonu za prikaz vaših uređa msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "Aplikaciji {productName} potreban je pristup snimanju zaslona. Dopustite snimanje zaslona u postavkama privatnosti operativnog sustava i ponovno pokrenite aplikaciju." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} se sada nalazi na adresi {host}. Instalirajte ga odande, a zatim se prijavite ovom aplikacijom." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} se sada nalazi na adresi {host}. Instalirajte ga odande." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Poziv dostupan" msgid "Call confirmation" msgstr "Potvrda poziva" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Poziv je u zasebnom prozoru" @@ -14361,6 +14361,11 @@ msgstr "Unesite svoje ime" msgid "Enter your password." msgstr "Unesite svoju lozinku." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Unesite svoj token za postavljanje" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Unesite svoje korisničko ime i ključ za oporavak iz svog kompleta za oporavak, a zatim odaberite novu lozinku." @@ -19448,7 +19453,7 @@ msgstr "Stara cijena" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Neka /tts pročita vašu poruku naglas. Onemogućavanjem ove postavke te se naredbe zadržavaju kao običan tekst." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Dopusti botovima u ovoj zajednici da vam šalju izravne poruke" @@ -25279,8 +25284,8 @@ msgstr "Plan" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Planovi, pogodnosti, pokloni i vaša pretplata nalaze se na jednoj stranici." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Planovi, pogodnosti, pokloni i vaša pretplata – sve je na jednoj stranici." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Izdvoji korisnika" msgid "Pop out video" msgstr "Izdvoji videozapis" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Izdvojeno" @@ -30120,6 +30125,11 @@ msgstr "Postavke." msgid "Setup key" msgstr "Ključ za postavljanje" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Token za postavljanje" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Oznaka #0000 rezervirana je za pretplatnike na Visionary. Visionary viš msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API se nalazi na drugom izvoru (origin) nego ova stranica, pa se zahtjevi za postavljanje šalju bez vaše sesije. Provjerite javni izvor i priključak za koje je ova instanca konfigurirana, a zatim ponovno učitajte stranicu." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API je token zapisao u svoj zapisnik pri prvom pokretanju. Ako ste postavili FLUXER_SETUP_TOKEN, upotrijebite tu vrijednost." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Aplikacija se srušila" @@ -33685,6 +33700,11 @@ msgstr "Sustav je dodijelio ulogu {role} korisniku {target}" msgid "The system removed the role {role} from {target}" msgstr "Sustav je uklonio ulogu {role} korisniku {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Token dokazuje da pokrećete ovu instancu. Samo osoba koja ga posjeduje može stvoriti administratorski račun." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Treća strana" msgid "This account can't be blocked." msgstr "Ovaj se račun ne može blokirati." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Ovaj račun nije administrator ove instance. Prijavite se s administratorskim računom da biste dovršili postavljanje." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/hu/messages.po b/fluxer_app/src/features/i18n/locales/hu/messages.po index 3f509eb52..293e294fe 100644 --- a/fluxer_app/src/features/i18n/locales/hu/messages.po +++ b/fluxer_app/src/features/i18n/locales/hu/messages.po @@ -1911,6 +1911,16 @@ msgstr "A(z) {productName} ide költözött: {host}. Telepítsd az új alkalmaz msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "A(z) {productName} ide költözött: {host}. Nyisd meg a böngésződben az új alkalmazás telepítéséhez." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "A(z) {productName} most itt: {host}. Innen telepítsd, majd jelentkezz be ezzel az alkalmazással." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "A(z) {productName} most itt: {host}. Telepítsd innen." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "A(z) {productName} mikrofon-hozzáférést igényel az eszközök listá msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "A(z) {productName} képernyőrögzítési hozzáférést igényel. Engedélyezd a képernyőrögzítést az operációs rendszer adatvédelmi beállításaiban, majd indítsd újra az alkalmazást." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "A(z) {productName} mostantól a(z) {host} címen érhető el. Telepítsd onnan, majd jelentkezz be ezzel az alkalmazással." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "A(z) {productName} mostantól itt érhető el: {host}. Telepítsd onnan." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Hívás elérhető" msgid "Call confirmation" msgstr "Hívás megerősítése" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "A hívás egy külön ablakban jelenik meg" @@ -14361,6 +14361,11 @@ msgstr "Add meg a nevedet" msgid "Enter your password." msgstr "Add meg a jelszavad." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Add meg a beállítási tokent" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Add meg a felhasználónevedet és a helyreállítási kulcsodat a helyreállítási csomagodból, majd válassz egy új jelszót." @@ -19448,7 +19453,7 @@ msgstr "Régi díjszabás" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "A /tts felolvassa az üzenetedet. A beállítás kikapcsolásával ezek a parancsok sima szövegként jelennek meg." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "A közösség botjai közvetlen üzeneteket küldhetnek neked" @@ -25279,8 +25284,8 @@ msgstr "Csomag" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "A csomagok, előnyök, ajándékok és az előfizetésed mind egy helyen találhatók." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Tervek, előnyök, ajándékok és az előfizetésed mind egy helyen." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Felhasználó megnyitása külön ablakban" msgid "Pop out video" msgstr "Videó megnyitása külön ablakban" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Külön ablakban megnyitva" @@ -30120,6 +30125,11 @@ msgstr "Beállítások." msgid "Setup key" msgstr "Beállítási kulcs" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Beállítási token" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "A #0000 címke a Visionary előfizetők számára van fenntartva. A Visi msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "Az API és ez az oldal eltérő webes eredethez tartozik, ezért a beállítási kérések nem tartalmazzák a munkamenetedet. Ellenőrizd a példányhoz megadott nyilvános eredetet és portot, majd töltsd újra az oldalt." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "Az API az első indításakor a naplójába írta a tokent. Ha beállítottad a FLUXER_SETUP_TOKEN értékét, azt használd." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Az alkalmazás összeomlott" @@ -33685,6 +33700,11 @@ msgstr "A rendszer a(z) {target} felhasználónak adta a(z) {role} szerepkört" msgid "The system removed the role {role} from {target}" msgstr "A rendszer eltávolította a(z) {role} szerepkört {target} felhasználótól" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "A token igazolja, hogy te futtatod ezt a példányt. Csak az hozhatja létre a rendszergazdai fiókot, akinél megvan." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Harmadik fél" msgid "This account can't be blocked." msgstr "Ez a fiók nem tiltható le." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Ez a fiók nem rendszergazdája ennek a példánynak. Jelentkezz be egy rendszergazdai fiókkal a beállítás befejezéséhez." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/id/messages.po b/fluxer_app/src/features/i18n/locales/id/messages.po index 0b9c6b0bb..2425bee12 100644 --- a/fluxer_app/src/features/i18n/locales/id/messages.po +++ b/fluxer_app/src/features/i18n/locales/id/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} telah pindah ke {host}. Instal aplikasi baru dan kamu akan msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} telah pindah ke {host}. Buka di browsermu untuk menginstal aplikasi baru." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} kini ada di {host}. Instal dari sana, lalu masuk dengan aplikasi ini." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} kini berada di {host}. Instal dari sana." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} memerlukan akses mikrofon untuk menampilkan daftar perangk msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} memerlukan akses perekaman layar. Izinkan perekaman layar di pengaturan privasi sistem operasimu dan mulai ulang aplikasi." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} kini berada di {host}. Instal dari sana, lalu masuk dengan aplikasi ini." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} kini berada di {host}. Instal dari sana." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Panggilan tersedia" msgid "Call confirmation" msgstr "Konfirmasi panggilan" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Panggilan dipindahkan ke jendela terpisah" @@ -14361,6 +14361,11 @@ msgstr "Masukkan namamu" msgid "Enter your password." msgstr "Masukkan kata sandimu." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Masukkan token penyiapan Anda" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Masukkan nama pengguna dan kunci pemulihan Anda dari kit pemulihan, lalu pilih kata sandi baru." @@ -19448,7 +19453,7 @@ msgstr "Harga lama" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Biarkan /tts membacakan pesanmu. Menonaktifkan pengaturan ini akan membuat perintah tersebut menjadi teks biasa." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Izinkan bot di komunitas ini mengirimi kamu DM" @@ -25279,8 +25284,8 @@ msgstr "Paket" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Paket, keuntungan, hadiah, dan langganan Anda semuanya ada di satu halaman." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Paket, keuntungan, hadiah, dan langganan Anda ada di satu halaman." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Buka pengguna di jendela terpisah" msgid "Pop out video" msgstr "Buka video di jendela terpisah" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Tampil di jendela terpisah" @@ -30120,6 +30125,11 @@ msgstr "Pengaturan." msgid "Setup key" msgstr "Kunci penyiapan" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Token penyiapan" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Tag #0000 dicadangkan untuk pelanggan Visionary. Visionary tidak lagi te msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API berada di origin yang berbeda dari halaman ini, jadi permintaan penyiapan dikirim tanpa sesimu. Periksa origin publik dan port yang dikonfigurasi untuk instance ini, lalu muat ulang." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API menulis token ke lognya saat pertama kali dimulai. Jika Anda mengatur FLUXER_SETUP_TOKEN, gunakan nilai tersebut." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Aplikasi mengalami error" @@ -33685,6 +33700,11 @@ msgstr "Sistem memberikan peran {role} kepada {target}" msgid "The system removed the role {role} from {target}" msgstr "Sistem menghapus peran {role} dari {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Token ini membuktikan bahwa Anda menjalankan instance ini. Hanya orang yang memilikinya yang dapat membuat akun admin." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Pihak ketiga" msgid "This account can't be blocked." msgstr "Akun ini tidak dapat diblokir." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Akun ini bukan admin dari instance ini. Masuk dengan akun admin untuk menyelesaikan penyiapan." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/it/messages.po b/fluxer_app/src/features/i18n/locales/it/messages.po index 7d1b6e662..6d729bcc2 100644 --- a/fluxer_app/src/features/i18n/locales/it/messages.po +++ b/fluxer_app/src/features/i18n/locales/it/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} si è spostato su {host}. Installa la nuova app e sarai gi msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} si è spostato su {host}. Aprilo nel browser per installare la nuova app." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} è ora su {host}. Installalo da lì, quindi accedi con questa app." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} è ora su {host}. Installalo da lì." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} ha bisogno dell'accesso al microfono per elencare i tuoi d msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} necessita dell'accesso alla registrazione dello schermo. Abilita la registrazione dello schermo nelle impostazioni sulla privacy del tuo sistema operativo e riavvia l'app." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} ora si trova su {host}. Installalo da lì, quindi accedi con questa app." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} ora si trova su {host}. Installalo da lì." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Chiamata disponibile" msgid "Call confirmation" msgstr "Conferma chiamata" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "La chiamata è in una finestra separata" @@ -14361,6 +14361,11 @@ msgstr "Inserisci il tuo nome" msgid "Enter your password." msgstr "Inserisci la tua password." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Inserisci il tuo token di configurazione" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Inserisci il tuo nome utente e la chiave di recupero dal tuo kit di recupero, quindi scegli una nuova password." @@ -19448,7 +19453,7 @@ msgstr "Tariffa precedente" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Fai leggere il tuo messaggio ad alta voce da /tts. Disattivando l'impostazione, questi comandi rimarranno come testo normale." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Consenti ai bot di questa community di inviarti messaggi diretti" @@ -25279,8 +25284,8 @@ msgstr "Piano" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Piani, vantaggi, regali e il tuo abbonamento si trovano tutti in un'unica pagina." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Piani, vantaggi, regali e il tuo abbonamento sono tutti in un'unica pagina." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Apri l’utente in una finestra separata" msgid "Pop out video" msgstr "Apri il video in una finestra separata" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "In finestra separata" @@ -30120,6 +30125,11 @@ msgstr "Impostazioni." msgid "Setup key" msgstr "Chiave di configurazione" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Token di configurazione" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Il tag #0000 è riservato agli abbonati Visionary. Visionary non è più msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "L'API si trova su un'origine diversa da questa pagina, quindi le richieste di configurazione vengono inviate senza la tua sessione. Controlla l'origine pubblica e la porta con cui è configurata questa istanza, quindi ricarica." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "L'API ha scritto il token nel suo log la prima volta che è stata avviata. Se hai impostato FLUXER_SETUP_TOKEN, usa quel valore." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "L'app si è bloccata" @@ -33685,6 +33700,11 @@ msgstr "Il sistema ha assegnato il ruolo {role} a {target}" msgid "The system removed the role {role} from {target}" msgstr "Il sistema ha rimosso il ruolo {role} da {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Il token dimostra che gestisci questa istanza. Solo la persona che lo possiede può creare l'account amministratore." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Terze parti" msgid "This account can't be blocked." msgstr "Questo account non può essere bloccato." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Questo account non è un amministratore di questa istanza. Accedi con un account amministratore per completare la configurazione." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/ja/messages.po b/fluxer_app/src/features/i18n/locales/ja/messages.po index b154ed81a..be89b01bd 100644 --- a/fluxer_app/src/features/i18n/locales/ja/messages.po +++ b/fluxer_app/src/features/i18n/locales/ja/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName}は{host}に移動しました。新しいアプリをイ msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName}は{host}に移動しました。ブラウザで開いて新しいアプリをインストールしてください。" +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} は {host} に移動しました。そちらからインストールし、このアプリでサインインしてください。" + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} は {host} に移動しました。そちらからインストールしてください。" + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName}がデバイスを一覧表示するには、マイクへ msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName}には画面収録へのアクセスが必要です。OSのプライバシー設定で画面収録を許可し、アプリを再起動してください。" -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName}は{host}に移動しました。そちらからインストールし、このアプリでサインインしてください。" - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName}は{host}に移動しました。そちらからインストールしてください。" - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "通話が進行中" msgid "Call confirmation" msgstr "通話確認" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "通話は別ウィンドウで表示中です" @@ -14361,6 +14361,11 @@ msgstr "名前を入力" msgid "Enter your password." msgstr "パスワードを入力してください。" +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "セットアップトークンを入力してください" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "ユーザー名とリカバリーキットのリカバリーキーを入力し、新しいパスワードを選択してください。" @@ -19448,7 +19453,7 @@ msgstr "旧料金" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "/ttsでメッセージを読み上げます。この設定をオフにすると、コマンドは通常のテキストとして表示されます。" -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "このコミュニティのボットからのDMを許可する" @@ -25279,7 +25284,7 @@ msgstr "プラン" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "プラン、特典、ギフト、サブスクリプションはすべて1つのページにまとめられています。" #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "ユーザーをポップアウト" msgid "Pop out video" msgstr "ビデオをポップアウト" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "ポップアウト中" @@ -30120,6 +30125,11 @@ msgstr "設定。" msgid "Setup key" msgstr "セットアップキー" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "セットアップトークン" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "#0000タグはVisionary登録者専用です。Visionaryは現在購入 msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "APIがこのページと異なるオリジンにあるため、セットアップリクエストはセッションなしで送信されます。このインスタンスで設定されているパブリックオリジンとポートを確認してから、再読み込みしてください。" +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "APIは初回起動時にトークンをログに記録しました。FLUXER_SETUP_TOKEN を設定した場合は、その値を使用してください。" + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "アプリがクラッシュしました" @@ -33685,6 +33700,11 @@ msgstr "システムが {target} に {role} のロールを付与しました" msgid "The system removed the role {role} from {target}" msgstr "システムが {role} のロールを {target} から削除しました" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "このトークンは、あなたがこのインスタンスを実行していることを証明します。これを持っている人だけが管理者アカウントを作成できます。" + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "サードパーティ" msgid "This account can't be blocked." msgstr "このアカウントをブロックすることはできません。" +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "このアカウントは、このインスタンスの管理者ではありません。設定を完了するには、管理者アカウントでサインインしてください。" + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/ko/messages.po b/fluxer_app/src/features/i18n/locales/ko/messages.po index 60ed8f227..875fc05c7 100644 --- a/fluxer_app/src/features/i18n/locales/ko/messages.po +++ b/fluxer_app/src/features/i18n/locales/ko/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName}이(가) {host}(으)로 이전했습니다. 새 앱을 설 msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName}이(가) {host}(으)로 이전했습니다. 새 앱을 설치하려면 브라우저에서 열어 주세요." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName}이(가) 이제 {host}(으)로 이전되었습니다. 해당 주소에서 설치한 후 이 앱으로 로그인하세요." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName}이(가) 이제 {host}(으)로 이전되었습니다. 해당 주소에서 설치하세요." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName}에서 기기 목록을 보려면 마이크 접근 권한 msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName}에 화면 녹화 접근 권한이 필요합니다. 운영체제 개인 정보 보호 설정에서 화면 녹화를 허용하고 앱을 다시 시작하세요." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName}이(가) 이제 {host}에 있습니다. 해당 주소에서 설치한 후 이 앱으로 로그인하세요." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName}이(가) 이제 {host}에 있습니다. 해당 주소에서 설치하세요." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "통화 참여 가능" msgid "Call confirmation" msgstr "통화 확인" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "통화가 팝업 창으로 열림" @@ -14361,6 +14361,11 @@ msgstr "이름을 입력하세요" msgid "Enter your password." msgstr "비밀번호를 입력하세요." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "설정 토큰을 입력하세요" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "복구 키트를 받은 복구 키와 사용자 이름을 입력한 다음, 새 비밀번호를 선택하세요." @@ -19448,7 +19453,7 @@ msgstr "기존 요금" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "/tts가 내 메시지를 소리 내어 읽도록 허용합니다. 이 설정을 비활성화하면 해당 명령은 일반 텍스트로 유지됩니다." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "이 커뮤니티의 봇이 나에게 DM을 보내도록 허용" @@ -25279,8 +25284,8 @@ msgstr "플랜" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "플랜, 혜택, 선물 및 구독 정보가 모두 한 페이지에 있습니다." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "플랜, 혜택, 선물 및 구독 정보를 한 페이지에서 모두 확인하세요." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "사용자 팝아웃" msgid "Pop out video" msgstr "영상 팝아웃" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "팝아웃됨" @@ -30120,6 +30125,11 @@ msgstr "설정." msgid "Setup key" msgstr "설정 키" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "설정 토큰" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "#0000 태그는 Visionary 구독자 전용입니다. Visionary는 더 msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API가 이 페이지와 다른 출처에 있어 설정 요청이 세션 없이 전송됩니다. 이 인스턴스에 설정된 공개 출처와 포트를 확인한 후 새로고침하세요." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API가 처음 시작될 때 토큰을 로그에 기록했습니다. FLUXER_SETUP_TOKEN을 설정했다면 해당 값을 사용하세요." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "앱이 비정상적으로 종료되었습니다" @@ -33685,6 +33700,11 @@ msgstr "시스템에서 {target}님에게 {role} 역할을 부여했습니다" msgid "The system removed the role {role} from {target}" msgstr "시스템에서 {role} 역할을 {target}님에게서 제거했습니다" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "토큰은 사용자가 이 인스턴스를 실행한다는 것을 증명합니다. 토큰을 가진 사람만이 관리자 계정을 만들 수 있습니다." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "타사" msgid "This account can't be blocked." msgstr "이 계정은 차단할 수 없습니다." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "이 계정은 이 인스턴스의 관리자가 아닙니다. 설정을 완료하려면 관리자 계정으로 로그인하세요." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/lt/messages.po b/fluxer_app/src/features/i18n/locales/lt/messages.po index e9e3baa57..d1e0072a8 100644 --- a/fluxer_app/src/features/i18n/locales/lt/messages.po +++ b/fluxer_app/src/features/i18n/locales/lt/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} perkeltas į {host}. Įdiekite naująją programėlę ir j msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} perkeltas į {host}. Atidarykite jį naršyklėje, kad įdiegtumėte naująją programėlę." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} dabar yra adresu {host}. Įdiekite jį iš ten, tada prisijunkite naudodami šią programą." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} dabar yra adresu {host}. Įdiekite jį iš ten." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} reikia prieigos prie mikrofono, kad būtų galima parodyti msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} reikia leidimo įrašyti ekraną. Leiskite įrašyti ekraną operacinės sistemos privatumo nustatymuose ir iš naujo paleiskite programą." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} dabar veikia adresu {host}. Įdiekite jį ten, tada prisijunkite naudodami šią programėlę." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} dabar veikia adresu {host}. Įdiekite jį iš ten." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Galimas skambutis" msgid "Call confirmation" msgstr "Skambučių patvirtinimas" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Skambutis atidarytas naujame lange" @@ -14361,6 +14361,11 @@ msgstr "Įveskite savo vardą" msgid "Enter your password." msgstr "Įveskite slaptažodį." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Įveskite savo sąrankos žetoną" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Įveskite savo vartotojo vardą ir atkūrimo rinkinio atkūrimo raktą, tada pasirinkite naują slaptažodį." @@ -19449,7 +19454,7 @@ msgstr "Senasis tarifas" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Leisti /tts perskaityti jūsų žinutę balsu. Išjungus šį nustatymą, šios komandos liks kaip įprastas tekstas." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Leisti šios bendruomenės botams siųsti jums tiesiogines žinutes" @@ -25280,8 +25285,8 @@ msgstr "Planas" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Planai, privilegijos, dovanos ir jūsų prenumerata – viskas vienoje vietoje." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Planai, privilegijos, dovanos ir jūsų prenumerata – viskas viename puslapyje." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25404,7 +25409,7 @@ msgstr "Atidaryti naudotoją atskirame lange" msgid "Pop out video" msgstr "Atidaryti vaizdą atskirame lange" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Atidaryta atskirame lange" @@ -30121,6 +30126,11 @@ msgstr "Nustatymai." msgid "Setup key" msgstr "Sąrankos raktas" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Sąrankos žetonas" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33429,6 +33439,11 @@ msgstr "Žyma #0000 skirta „Visionary“ prenumeratoriams. „Visionary“ neb msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API veikia kitoje kilmėje nei šis puslapis, todėl sąrankos užklausos siunčiamos be jūsų seanso. Patikrinkite viešąją kilmę ir prievadą, su kuriais sukonfigūruota ši instancija, tada įkelkite puslapį iš naujo." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API pirmą kartą paleidžiant žetoną įrašė į savo žurnalą. Jei nustatėte FLUXER_SETUP_TOKEN, naudokite tą reikšmę." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Programa nulūžo" @@ -33686,6 +33701,11 @@ msgstr "Sistema suteikė {target} vaidmenį {role}" msgid "The system removed the role {role} from {target}" msgstr "Sistema pašalino vaidmenį {role} iš {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Šis žetonas įrodo, kad paleidote šią instanciją. Tik tas, kas jį turi, gali sukurti administratoriaus paskyrą." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33904,6 +33924,11 @@ msgstr "Trečioji šalis" msgid "This account can't be blocked." msgstr "Šios paskyros negalima užblokuoti." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Ši paskyra nėra šios instancijos administratorius. Norėdami užbaigti sąranką, prisijunkite naudodami administratoriaus paskyrą." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/nl/messages.po b/fluxer_app/src/features/i18n/locales/nl/messages.po index 3acc8ceca..f36fff04f 100644 --- a/fluxer_app/src/features/i18n/locales/nl/messages.po +++ b/fluxer_app/src/features/i18n/locales/nl/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} is verhuisd naar {host}. Installeer de nieuwe app en je be msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} is verhuisd naar {host}. Open het in je browser om de nieuwe app te installeren." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} is nu op {host}. Installeer het vanaf daar en meld je vervolgens aan met deze app." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} is nu op {host}. Installeer het vanaf daar." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} heeft microfoontoegang nodig om je apparaten weer te geven msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} heeft toestemming nodig om het scherm op te nemen. Sta schermopname toe in de privacyinstellingen van je besturingssysteem en start de app opnieuw." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} is nu te vinden op {host}. Installeer het vanaf daar en log vervolgens in met deze app." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} is nu te vinden op {host}. Installeer het vanaf daar." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Gesprek beschikbaar" msgid "Call confirmation" msgstr "Oproepbevestiging" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Gesprek in apart venster" @@ -14361,6 +14361,11 @@ msgstr "Voer je naam in" msgid "Enter your password." msgstr "Voer je wachtwoord in." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Voer uw instellingstoken in" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Voer uw gebruikersnaam en de herstelsleutel uit uw herstelkit in, kies dan een nieuw wachtwoord." @@ -19448,7 +19453,7 @@ msgstr "Oud tarief" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Laat /tts je bericht voorlezen. Als je de instelling uitschakelt, blijven deze commando's als normale tekst zichtbaar." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Laat bots in deze community je DM's sturen" @@ -25279,8 +25284,8 @@ msgstr "Abonnement" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Abonnementen, voordelen, cadeaus en je abonnement vind je allemaal op één pagina." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Abonnementen, voordelen, cadeaus en je abonnement staan allemaal op één pagina." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Gebruiker in apart venster openen" msgid "Pop out video" msgstr "Video in apart venster openen" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "In apart venster" @@ -30120,6 +30125,11 @@ msgstr "Instellingen." msgid "Setup key" msgstr "Configuratiesleutel" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Instellingstoken" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "De tag #0000 is gereserveerd voor Visionary-abonnees. Visionary is niet msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "De API gebruikt een andere origin dan deze pagina. Daardoor worden installatieverzoeken zonder je sessie verstuurd. Controleer de openbare origin en poort die voor deze instantie zijn ingesteld en laad de pagina opnieuw." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "De API heeft de token de eerste keer dat deze werd gestart naar het logboek geschreven. Als je FLUXER_SETUP_TOKEN hebt ingesteld, gebruik dan die waarde." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "De app is gecrasht" @@ -33685,6 +33700,11 @@ msgstr "Het systeem heeft {target} de rol {role} gegeven" msgid "The system removed the role {role} from {target}" msgstr "Het systeem heeft de rol {role} van {target} verwijderd" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "De token bewijst dat jij deze instantie beheert. Alleen de persoon die hem heeft, kan het beheerdersaccount aanmaken." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Derden" msgid "This account can't be blocked." msgstr "Dit account kan niet worden geblokkeerd." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Dit account is geen beheerder van deze instantie. Log in met een beheerdersaccount om de installatie te voltooien." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/no/messages.po b/fluxer_app/src/features/i18n/locales/no/messages.po index e5303781f..6cd85dad7 100644 --- a/fluxer_app/src/features/i18n/locales/no/messages.po +++ b/fluxer_app/src/features/i18n/locales/no/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} har flyttet til {host}. Installer den nye appen, så er du msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} har flyttet til {host}. Åpne det i nettleseren for å installere den nye appen." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} er nå på {host}. Installer det derfra, og logg deretter inn med denne appen." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} er nå på {host}. Installer det derfra." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} trenger mikrofontilgang for å vise enhetene dine." msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} trenger tilgang til skjermopptak. Tillat skjermopptak i operativsystemets personverninnstillinger og start appen på nytt." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} holder nå til på {host}. Installer det derfra, og logg deretter på med denne appen." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} holder nå til på {host}. Installer det derfra." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Samtale tilgjengelig" msgid "Call confirmation" msgstr "Anropsbekreftelse" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Samtalen er i et eget vindu" @@ -14361,6 +14361,11 @@ msgstr "Skriv inn navnet ditt" msgid "Enter your password." msgstr "Skriv inn passordet ditt." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Skriv inn oppsettstokenen din" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Skriv inn brukernavnet ditt og gjenopprettingsnøkkelen fra gjenopprettingssettet ditt, og velg deretter et nytt passord." @@ -19448,7 +19453,7 @@ msgstr "Gammel pris" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "La /tts lese meldingen din høyt. Hvis du deaktiverer innstillingen, blir disse kommandoene vanlig tekst." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "La boter i dette fellesskapet sende deg direktemeldinger" @@ -25279,8 +25284,8 @@ msgstr "Abonnement" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Planer, fordeler, gaver og abonnementet ditt finner du alt på én side." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Planer, fordeler, gaver og abonnementet ditt er alt på én side." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Vis bruker i eget vindu" msgid "Pop out video" msgstr "Vis video i eget vindu" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Åpnet i eget vindu" @@ -30120,6 +30125,11 @@ msgstr "Innstillinger." msgid "Setup key" msgstr "Oppsettsnøkkel" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Oppsettstoken" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Taggen #0000 er reservert for Visionary-abonnenter. Visionary er ikke le msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API-et bruker en annen origin enn denne siden, så oppsettsforespørsler sendes uten økten din. Kontroller instansens konfigurerte offentlige origin og port, og last inn på nytt." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API-en skrev tokenet til loggen sin første gang den startet. Hvis du setter FLUXER_SETUP_TOKEN, bruk den verdien." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Appen krasjet" @@ -33685,6 +33700,11 @@ msgstr "Systemet ga {target} rollen {role}" msgid "The system removed the role {role} from {target}" msgstr "Systemet fjernet rollen {role} fra {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Tokenet beviser at du kjører denne instansen. Kun personen som har det, kan opprette administratorkontoen." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Tredjepart" msgid "This account can't be blocked." msgstr "Denne kontoen kan ikke blokkeres." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Denne kontoen er ikke administrator for denne instansen. Logg inn med en administratorkonto for å fullføre oppsettet." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/pl/messages.po b/fluxer_app/src/features/i18n/locales/pl/messages.po index 602987fbd..825ead8e7 100644 --- a/fluxer_app/src/features/i18n/locales/pl/messages.po +++ b/fluxer_app/src/features/i18n/locales/pl/messages.po @@ -1911,6 +1911,16 @@ msgstr "Aplikacja {productName} została przeniesiona do {host}. Zainstaluj now msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "Aplikacja {productName} została przeniesiona do {host}. Otwórz ją w przeglądarce, aby zainstalować nową aplikację." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} jest teraz pod adresem {host}. Zainstaluj go stamtąd, a następnie zaloguj się za pomocą tej aplikacji." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} jest teraz pod adresem {host}. Zainstaluj go stamtąd." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} potrzebuje dostępu do mikrofonu, aby wyświetlić Twoje u msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} potrzebuje dostępu do nagrywania ekranu. Włącz nagrywanie ekranu w ustawieniach prywatności systemu operacyjnego i uruchom ponownie aplikację." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "Aplikacja {productName} jest teraz dostępna pod adresem {host}. Zainstaluj ją stamtąd, a następnie zaloguj się za pomocą tej aplikacji." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "Aplikacja {productName} jest teraz dostępna pod adresem {host}. Zainstaluj ją stamtąd." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Połączenie dostępne" msgid "Call confirmation" msgstr "Potwierdzenie połączenia" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Połączenie jest wyświetlane w osobnym oknie" @@ -14361,6 +14361,11 @@ msgstr "Wpisz swoje imię" msgid "Enter your password." msgstr "Wprowadź hasło." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Wpisz swój token konfiguracyjny" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Wpisz swoją nazwę użytkownika i klucz odzyskiwania z zestawu odzyskiwania, a następnie wybierz nowe hasło." @@ -19448,7 +19453,7 @@ msgstr "Stawka historyczna" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Pozwól /tts odczytać wiadomość na głos. Wyłączenie tego ustawienia sprawi, że polecenia będą wyświetlane jako zwykły tekst." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Zezwól botom z tej społeczności na wysyłanie Ci wiadomości prywatnych" @@ -25279,8 +25284,8 @@ msgstr "Plan" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Plany, korzyści, prezenty i Twoja subskrypcja – wszystko znajduje się na jednej stronie." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Plany, korzyści, prezenty i Twoja subskrypcja – wszystko na jednej stronie." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Wyświetl użytkownika w osobnym oknie" msgid "Pop out video" msgstr "Wyświetl wideo w osobnym oknie" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Wyświetlane w oddzielnym oknie" @@ -30120,6 +30125,11 @@ msgstr "Ustawienia." msgid "Setup key" msgstr "Klucz konfiguracji" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Token konfiguracji" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Tag #0000 jest zarezerwowany dla subskrybentów Visionary. Nie można ju msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API ma inne pochodzenie niż ta strona, więc żądania konfiguracji są wysyłane bez Twojej sesji. Sprawdź publiczne pochodzenie i port skonfigurowane dla tej instancji, a następnie przeładuj stronę." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "Token został zapisany w logach API przy pierwszym uruchomieniu. Jeśli ustawiłeś FLUXER_SETUP_TOKEN, użyj tej wartości." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Aplikacja uległa awarii" @@ -33685,6 +33700,11 @@ msgstr "System nadał {target} rolę {role}" msgid "The system removed the role {role} from {target}" msgstr "System usunął rolę {role} użytkownikowi {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Token potwierdza, że uruchamiasz tę instancję. Tylko osoba, która go posiada, może utworzyć konto administratora." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Inne firmy" msgid "This account can't be blocked." msgstr "Tego konta nie można zablokować." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "To konto nie jest administratorem tej instancji. Zaloguj się przy użyciu konta administratora, aby dokończyć konfigurację." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/pt-BR/messages.po b/fluxer_app/src/features/i18n/locales/pt-BR/messages.po index bce0a4153..e00a3882f 100644 --- a/fluxer_app/src/features/i18n/locales/pt-BR/messages.po +++ b/fluxer_app/src/features/i18n/locales/pt-BR/messages.po @@ -1911,6 +1911,16 @@ msgstr "O {productName} mudou para {host}. Instale o novo app e você já estar msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "O {productName} mudou para {host}. Abra-o no seu navegador para instalar o novo app." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} agora está em {host}. Instale-o de lá e, em seguida, faça login com este app." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} agora está em {host}. Instale-o a partir daí." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "O {productName} precisa de acesso ao microfone para listar seus disposit msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} precisa de acesso à gravação de tela. Permita a gravação de tela nas configurações de privacidade do seu sistema operacional e reinicie o aplicativo." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "O {productName} agora está em {host}. Instale-o de lá e depois faça login com este app." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "O {productName} agora está em {host}. Instale-o de lá." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Chamada disponível" msgid "Call confirmation" msgstr "Confirmação de chamada" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Chamada em janela separada" @@ -14361,6 +14361,11 @@ msgstr "Digite seu nome" msgid "Enter your password." msgstr "Digite sua senha." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Insira seu token de configuração" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Digite seu nome de usuário e a chave de recuperação do seu kit, depois escolha uma nova senha." @@ -19448,7 +19453,7 @@ msgstr "Preço antigo" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Deixe o /tts ler sua mensagem em voz alta. Desativar essa opção mantém esses comandos como texto normal." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Permitir que os bots desta comunidade enviem mensagens diretas para você" @@ -25279,8 +25284,8 @@ msgstr "Plano" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Planos, vantagens, presentes e sua assinatura ficam todos em uma página." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Planos, vantagens, presentes e sua assinatura em uma só página." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Abrir usuário em janela separada" msgid "Pop out video" msgstr "Abrir vídeo em janela separada" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Em janela separada" @@ -30120,6 +30125,11 @@ msgstr "Configurações." msgid "Setup key" msgstr "Chave de configuração" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Token de configuração" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "A tag #0000 é reservada para assinantes Visionary. O Visionary não est msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "A API está em uma origem diferente desta página, então as solicitações de configuração são enviadas sem sua sessão. Verifique a origem pública e a porta com as quais esta instância está configurada e recarregue." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "A API gravou o token no log dela na primeira vez que iniciou. Se você definiu FLUXER_SETUP_TOKEN, use esse valor." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "O app travou" @@ -33685,6 +33700,11 @@ msgstr "O sistema deu a {target} o cargo {role}" msgid "The system removed the role {role} from {target}" msgstr "O sistema removeu o cargo {role} de {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "O token prova que você executa esta instância. Somente a pessoa que o possui pode criar a conta de administrador." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Terceiros" msgid "This account can't be blocked." msgstr "Não é possível bloquear esta conta." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Esta conta não é de um administrador desta instância. Faça login com uma conta de administrador para concluir a configuração." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/ro/messages.po b/fluxer_app/src/features/i18n/locales/ro/messages.po index f99a566d5..9f45667f2 100644 --- a/fluxer_app/src/features/i18n/locales/ro/messages.po +++ b/fluxer_app/src/features/i18n/locales/ro/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} s-a mutat pe {host}. Instalează noua aplicație și vei f msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} s-a mutat pe {host}. Deschide-l în browser pentru a instala noua aplicație." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} se află acum la adresa {host}. Instalează-l de acolo, apoi conectează-te cu această aplicație." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} se află acum la {host}. Instalează-l de acolo." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} are nevoie de acces la microfon pentru a-ți afișa dispoz msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} are nevoie de acces la înregistrarea ecranului. Permite înregistrarea ecranului în setările de confidențialitate ale sistemului de operare și repornește aplicația." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} se află acum la adresa {host}. Instalează-l de acolo, apoi conectează-te cu această aplicație." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} se află acum la adresa {host}. Instalează-l de acolo." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Apel disponibil" msgid "Call confirmation" msgstr "Confirmare apel" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Apelul este într-o fereastră separată" @@ -14361,6 +14361,11 @@ msgstr "Introdu-ți numele" msgid "Enter your password." msgstr "Introdu parola." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Introdu tokenul de configurare" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Introdu numele de utilizator și cheia de recuperare din kitul tău de recuperare, apoi alege o parolă nouă." @@ -19448,7 +19453,7 @@ msgstr "Tarif vechi" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Permite ca /tts să-ți citească mesajul cu voce tare. Dezactivarea setării păstrează aceste comenzi ca text obișnuit." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Permite boților din această comunitate să-ți trimită DM-uri" @@ -25279,8 +25284,8 @@ msgstr "Abonament" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Planurile, beneficiile, cadourile și abonamentul tău se găsesc pe o singură pagină." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Planuri, beneficii, cadouri și abonamentul tău sunt toate pe o singură pagină." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Deschide utilizatorul într-o fereastră separată" msgid "Pop out video" msgstr "Deschide videoclipul într-o fereastră separată" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "În fereastră separată" @@ -30120,6 +30125,11 @@ msgstr "Setări." msgid "Setup key" msgstr "Cheie de configurare" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Token de configurare" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Tagul #0000 este rezervat abonaților Visionary. Abonamentul Visionary n msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API-ul este pe un alt domeniu decât această pagină, așa că cererile de configurare sunt trimise fără sesiunea ta. Verifică domeniul public și portul cu care este configurată această instanță, apoi reîncarcă." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API-ul a scris tokenul în jurnalul său la prima pornire. Dacă ați setat FLUXER_SETUP_TOKEN, folosiți acea valoare." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Aplicația s-a închis neașteptat" @@ -33685,6 +33700,11 @@ msgstr "Sistemul i-a acordat rolul {role} lui {target}" msgid "The system removed the role {role} from {target}" msgstr "Sistemul a eliminat rolul {role} de la {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Tokenul dovedește că rulezi această instanță. Doar persoana care îl deține poate crea contul de administrator." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Terță parte" msgid "This account can't be blocked." msgstr "Acest cont nu poate fi blocat." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Acest cont nu este de tip administrator pentru această instanță. Conectați-vă cu un cont de administrator pentru a finaliza configurarea." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/ru/messages.po b/fluxer_app/src/features/i18n/locales/ru/messages.po index 487439e98..6307f2afa 100644 --- a/fluxer_app/src/features/i18n/locales/ru/messages.po +++ b/fluxer_app/src/features/i18n/locales/ru/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} переехал на {host}. Установите нов msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} переехал на {host}. Откройте его в браузере, чтобы установить новое приложение." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} теперь находится по адресу {host}. Установите его оттуда, а затем войдите в систему с помощью этого приложения." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} теперь находится по адресу {host}. Установите его оттуда." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} требуется доступ к микрофону, ч msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} требуется доступ к записи экрана. Разрешите запись экрана в настройках конфиденциальности вашей операционной системы и перезапустите приложение." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} теперь находится по адресу {host}. Установите его оттуда, а затем войдите через это приложение." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} теперь находится по адресу {host}. Установите его оттуда." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Звонок доступен" msgid "Call confirmation" msgstr "Подтверждение звонка" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Звонок вынесен в отдельное окно" @@ -14361,6 +14361,11 @@ msgstr "Введите ваше имя" msgid "Enter your password." msgstr "Введите пароль." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Введите ваш токен настройки" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Введите ваше имя пользователя и ключ восстановления из вашего набора для восстановления, затем выберите новый пароль." @@ -19448,7 +19453,7 @@ msgstr "Старый тариф" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Разрешить /tts зачитывать ваши сообщения вслух. Если настройка выключена, такие команды остаются обычным текстом." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Разрешить ботам из этого сообщества отправлять вам личные сообщения" @@ -25279,7 +25284,7 @@ msgstr "Тариф" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "Тарифы, бонусы, подарки и ваша подписка — всё на одной странице." #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "Открыть пользователя в отдельном окне" msgid "Pop out video" msgstr "Открыть видео в отдельном окне" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "В отдельном окне" @@ -30120,6 +30125,11 @@ msgstr "Настройки." msgid "Setup key" msgstr "Ключ настройки" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Токен настройки" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Тег #0000 зарезервирован для подписчиков msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API находится на другом домене, чем эта страница, поэтому запросы на настройку отправляются без вашей сессии. Проверьте общедоступный домен и порт, с которыми настроен этот экземпляр, затем перезагрузите страницу." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API записал токен в свой журнал при первом запуске. Если вы установили FLUXER_SETUP_TOKEN, используйте это значение." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Приложение вылетело" @@ -33685,6 +33700,11 @@ msgstr "Система выдала пользователю {target} роль { msgid "The system removed the role {role} from {target}" msgstr "Система автоматически удалила роль {role} у {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Токен подтверждает, что вы управляете этим экземпляром. Только владелец токена может создать учетную запись администратора." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Сторонние" msgid "This account can't be blocked." msgstr "Этот аккаунт нельзя заблокировать." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Эта учетная запись не является администратором этого экземпляра. Войдите с учетной записью администратора, чтобы завершить настройку." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/sv-SE/messages.po b/fluxer_app/src/features/i18n/locales/sv-SE/messages.po index c8c9a177f..04ce2100b 100644 --- a/fluxer_app/src/features/i18n/locales/sv-SE/messages.po +++ b/fluxer_app/src/features/i18n/locales/sv-SE/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} har flyttat till {host}. Installera den nya appen så är msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} har flyttat till {host}. Öppna det i webbläsaren för att installera den nya appen." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} finns nu på {host}. Installera det därifrån och logga sedan in med den här appen." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} finns nu på {host}. Installera det därifrån." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} behöver mikrofonåtkomst för att visa dina enheter." msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} behöver åtkomst till skärminspelning. Tillåt skärminspelning i operativsystemets sekretessinställningar och starta om appen." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} finns nu på {host}. Installera det därifrån och logga sedan in med den här appen." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} finns nu på {host}. Installera det därifrån." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Samtal tillgängligt" msgid "Call confirmation" msgstr "Samtalsbekräftelse" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Samtalet är öppnat i ett separat fönster" @@ -14361,6 +14361,11 @@ msgstr "Ange ditt namn" msgid "Enter your password." msgstr "Ange ditt lösenord." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Ange din konfigurationstoken" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Ange ditt användarnamn och återställningsnyckeln från ditt återställningskit, välj sedan ett nytt lösenord." @@ -19448,7 +19453,7 @@ msgstr "Äldre pris" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Låt /tts läsa upp ditt meddelande. Om du inaktiverar inställningen visas dessa kommandon som vanlig text." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Låt botar i den här communityn skicka direktmeddelanden till dig" @@ -25279,8 +25284,8 @@ msgstr "Plan" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Prenumerationer, förmåner, gåvor och ditt abonnemang finns alla på en sida." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Prenumerationer, förmåner, gåvor och ditt abonnemang finns allt på en sida." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Öppna användare i eget fönster" msgid "Pop out video" msgstr "Visa video i eget fönster" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Visas i separat fönster" @@ -30120,6 +30125,11 @@ msgstr "Inställningar." msgid "Setup key" msgstr "Konfigurationsnyckel" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Konfigurationstoken" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Taggen #0000 är reserverad för Visionary-medlemmar. Visionary kan inte msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API:et har ett annat ursprung än den här sidan, så konfigurationsförfrågningar skickas utan din session. Kontrollera vilket offentligt ursprung och vilken port instansen är konfigurerad med och ladda sedan om." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API:et skrev tokenen till sin logg första gången det startade. Om du har angett FLUXER_SETUP_TOKEN använder du det värdet." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Appen kraschade" @@ -33685,6 +33700,11 @@ msgstr "Systemet gav {target} rollen {role}" msgid "The system removed the role {role} from {target}" msgstr "Systemet tog bort rollen {role} från {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Tokenen bevisar att du driver den här instansen. Bara den som har den kan skapa administratörskontot." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Tredje part" msgid "This account can't be blocked." msgstr "Det här kontot kan inte blockeras." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Det här kontot är inte administratör för den här instansen. Logga in med ett administratörskonto för att slutföra installationen." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/th/messages.po b/fluxer_app/src/features/i18n/locales/th/messages.po index 2fbe07464..83bf0b2df 100644 --- a/fluxer_app/src/features/i18n/locales/th/messages.po +++ b/fluxer_app/src/features/i18n/locales/th/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} ได้ย้ายไปยัง {host} แล้ว msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} ย้ายไปที่ {host} แล้ว เปิดในเบราว์เซอร์ของคุณเพื่อติดตั้งแอปใหม่" +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} ตอนนี้อยู่ที่ {host} แล้ว ติดตั้งจากที่นั่น จากนั้นลงชื่อเข้าใช้ด้วยแอปนี้" + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} ตอนนี้อยู่ที่ {host} แล้ว ติดตั้งจากที่นั่นได้เลย" + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} ต้องการสิทธิ์เข้าถึ msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} ต้องการสิทธิ์บันทึกหน้าจอ โปรดอนุญาตการบันทึกหน้าจอในการตั้งค่าความเป็นส่วนตัวของระบบปฏิบัติการ แล้วรีสตาร์ทแอป" -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} ตอนนี้อยู่ที่ {host} แล้ว ติดตั้งจากที่นั่น จากนั้นลงชื่อเข้าใช้ด้วยแอปนี้" - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} ตอนนี้อยู่ที่ {host} แล้ว ติดตั้งจากที่นั่นได้เลย" - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "มีสายให้เข้าร่วม" msgid "Call confirmation" msgstr "ยืนยันการโทร" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "การโทรอยู่ในหน้าต่างแยก" @@ -14361,6 +14361,11 @@ msgstr "ป้อนชื่อของคุณ" msgid "Enter your password." msgstr "ป้อนรหัสผ่านของคุณ" +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "ใส่โทเค็นการตั้งค่าของคุณ" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "กรอกชื่อผู้ใช้และคีย์กู้คืนจากชุดกู้คืนของคุณ จากนั้นเลือกรหัสผ่านใหม่" @@ -19448,7 +19453,7 @@ msgstr "เรทราคาเดิม" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "ให้ /tts อ่านข้อความของคุณออกเสียง การปิดการตั้งค่านี้จะทำให้คำสั่งเหล่านั้นเป็นข้อความปกติ" -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "ให้บอทในคอมมูนิตี้นี้ส่งข้อความส่วนตัวถึงคุณได้" @@ -25279,8 +25284,8 @@ msgstr "แพ็กเกจ" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "แผน สิทธิประโยชน์ ของขวัญ และการสมัครสมาชิกของคุณทั้งหมดจะอยู่ที่หน้าเดียว" +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "แผน สิทธิประโยชน์ ของขวัญ และการสมัครใช้งานของคุณ ทั้งหมดอยู่ในหน้าเดียว" #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "เปิดผู้ใช้ในหน้าต่างแยก" msgid "Pop out video" msgstr "เปิดวิดีโอในหน้าต่างแยก" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "อยู่ในหน้าต่างแยก" @@ -30120,6 +30125,11 @@ msgstr "การตั้งค่า" msgid "Setup key" msgstr "คีย์การตั้งค่า" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "โทเค็นการตั้งค่า" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "แท็ก #0000 สงวนไว้สำหรับสมาช msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API อยู่คนละ origin กับหน้านี้ ทำให้คำขอตั้งค่าถูกส่งไปโดยไม่มีเซสชันของคุณ โปรดตรวจสอบ public origin และพอร์ตที่อินสแตนซ์นี้กำหนดค่าไว้ แล้วลองโหลดใหม่อีกครั้ง" +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API ได้เขียนโทเค็นลงในบันทึกเมื่อเริ่มทำงานครั้งแรก หากคุณตั้งค่า FLUXER_SETUP_TOKEN ให้ใช้ค่านั้น" + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "แอปขัดข้อง" @@ -33685,6 +33700,11 @@ msgstr "ระบบให้บทบาท {role} แก่ {target}" msgid "The system removed the role {role} from {target}" msgstr "ระบบนำบทบาท {role} ออกจาก {target} แล้ว" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "โทเค็นนี้เป็นการยืนยันว่าคุณเป็นผู้ดูแลระบบนี้ เฉพาะผู้ที่มีโทเค็นนี้เท่านั้นที่สามารถสร้างบัญชีผู้ดูแลระบบได้" + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "บุคคลภายนอก" msgid "This account can't be blocked." msgstr "ไม่สามารถบล็อกบัญชีนี้ได้" +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "บัญชีนี้ไม่ใช่ผู้ดูแลระบบของอินสแตนซ์นี้ ลงชื่อเข้าใช้ด้วยบัญชีผู้ดูแลระบบเพื่อตั้งค่าให้เสร็จสมบูรณ์" + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/tr/messages.po b/fluxer_app/src/features/i18n/locales/tr/messages.po index 1f041b801..c10152d1f 100644 --- a/fluxer_app/src/features/i18n/locales/tr/messages.po +++ b/fluxer_app/src/features/i18n/locales/tr/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName}, {host} adresine taşındı. Yeni uygulamayı yüklediğin msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName}, {host} adresine taşındı. Yeni uygulamayı yüklemek için tarayıcınızda açın." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName}, artık {host} adresinde. Oradan yükleyin, ardından bu uygulamayla giriş yapın." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} artık {host} adresinde. Oradan yükleyin." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} uygulamasının cihazlarınızı listelemek için mikrofon msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} uygulamasının ekran kaydı erişimine ihtiyacı var. İşletim sisteminizin gizlilik ayarlarından ekran kaydına izin verin ve uygulamayı yeniden başlatın." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} artık {host} adresinde. Oradan yükleyin, ardından bu uygulamayla giriş yapın." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} artık {host} adresinde. Oradan yükleyin." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Devam eden arama" msgid "Call confirmation" msgstr "Arama onayı" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Arama ayrı bir pencerede açıldı" @@ -14361,6 +14361,11 @@ msgstr "Adınızı girin" msgid "Enter your password." msgstr "Parolanızı girin." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Kurulum belirtecini girin" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Kurtarma anahtarınızı ve kurtarma kitinizdeki kurtarma anahtarını girin, ardından yeni bir şifre seçin." @@ -19448,7 +19453,7 @@ msgstr "Eski tarife" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "/tts mesajınızı sesli okusun. Bu ayar kapalıyken bu komutlar normal metin olarak kalır." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Bu topluluktaki botların size DM göndermesine izin ver" @@ -25279,7 +25284,7 @@ msgstr "Plan" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "Planlar, avantajlar, hediyeler ve aboneliğiniz tek bir sayfada." #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "Kullanıcıyı ayrı pencerede göster" msgid "Pop out video" msgstr "Videoyu ayrı pencerede göster" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Ayrı bir pencerede" @@ -30120,6 +30125,11 @@ msgstr "Ayarlar." msgid "Setup key" msgstr "Kurulum anahtarı" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Kurulum belirteci" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "#0000 etiketi Visionary abonelerine özeldir. Visionary artık satın al msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API bu sayfadan farklı bir kaynakta olduğu için kurulum istekleri oturumunuz olmadan gönderiliyor. Bu kurulum için yapılandırılan genel kaynağı ve portu kontrol edin, ardından sayfayı yeniden yükleyin." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API, ilk başladığında belirteci günlüğüne yazdı. FLUXER_SETUP_TOKEN'ı ayarladıysanız, bu değeri kullanın." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Uygulama çöktü" @@ -33685,6 +33700,11 @@ msgstr "Sistem, {target} adlı kullanıcıya {role} rolünü verdi" msgid "The system removed the role {role} from {target}" msgstr "Sistem, {role} rolünü {target} adlı kullanıcıdan kaldırdı" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Bu belirteç, bu örneği sizin çalıştırdığınızı kanıtlar. Yalnızca buna sahip olan kişi yönetici hesabı oluşturabilir." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Üçüncü taraf" msgid "This account can't be blocked." msgstr "Bu hesap engellenemez." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Bu hesap bu örneğin yöneticisi değil. Kurulumu tamamlamak için bir yönetici hesabıyla oturum açın." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/uk/messages.po b/fluxer_app/src/features/i18n/locales/uk/messages.po index da0c2f4db..e96590685 100644 --- a/fluxer_app/src/features/i18n/locales/uk/messages.po +++ b/fluxer_app/src/features/i18n/locales/uk/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} переїхав на {host}. Установіть нов msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} переїхав на {host}. Відкрийте його у браузері, щоб установити новий застосунок." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} тепер за адресою {host}. Встановіть його звідти, а потім увійдіть за допомогою цієї програми." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} тепер за адресою {host}. Встановіть звідти." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "Щоб переглянути список пристроїв, заст msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} потрібен доступ до запису екрана. Увімкніть запис екрана в налаштуваннях конфіденційності вашої операційної системи та перезапустіть застосунок." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} тепер доступний на {host}. Встановіть його звідти, а потім увійдіть за допомогою цього застосунку." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} тепер доступний на {host}. Встановіть його звідти." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Дзвінок доступний" msgid "Call confirmation" msgstr "Підтвердження дзвінка" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Дзвінок відкрито в окремому вікні" @@ -14361,6 +14361,11 @@ msgstr "Введіть своє ім'я" msgid "Enter your password." msgstr "Введіть свій пароль." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Введіть ваш токен налаштування" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Введіть ім'я користувача та ключ відновлення з вашого комплекту відновлення, а потім виберіть новий пароль." @@ -19448,7 +19453,7 @@ msgstr "Старий тариф" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Дозволити /tts читати ваші повідомлення вголос. Вимкнення цієї функції залишить ці команди як звичайний текст." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Дозволити ботам цієї спільноти надсилати вам особисті повідомлення" @@ -25279,8 +25284,8 @@ msgstr "План" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "Тарифи, переваги, подарунки та ваша підписка — усе це на одній сторінці." +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "Тарифні плани, переваги, подарунки та ваша підписка – усе на одній сторінці." #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "Відкрити користувача в окремому вікні" msgid "Pop out video" msgstr "Відкрити відео в окремому вікні" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "В окремому вікні" @@ -30120,6 +30125,11 @@ msgstr "Налаштування." msgid "Setup key" msgstr "Ключ налаштування" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Токен налаштування" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Тег #0000 зарезервовано для підписників Vi msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API знаходиться на іншому домені, ніж ця сторінка, тому запити на налаштування надсилаються без вашої сесії. Перевірте загальнодоступний домен і порт, з якими налаштовано цей екземпляр, а потім перезавантажте." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API записав токен у свій журнал під час першого запуску. Якщо ви встановили FLUXER_SETUP_TOKEN, використовуйте це значення." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Сталася помилка в застосунку" @@ -33685,6 +33700,11 @@ msgstr "Система надала {target} роль {role}" msgid "The system removed the role {role} from {target}" msgstr "Система видалила роль {role} у користувача {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Цей токен підтверджує, що ви керуєте цим екземпляром. Лише особа, яка його має, може створити обліковий запис адміністратора." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Сторонні" msgid "This account can't be blocked." msgstr "Цей обліковий запис не можна заблокувати." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Цей обліковий запис не є адміністратором цього екземпляра. Увійдіть за допомогою облікового запису адміністратора, щоб завершити налаштування." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/vi/messages.po b/fluxer_app/src/features/i18n/locales/vi/messages.po index 7547d18c1..2643d98c7 100644 --- a/fluxer_app/src/features/i18n/locales/vi/messages.po +++ b/fluxer_app/src/features/i18n/locales/vi/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} đã chuyển sang {host}. Hãy cài đặt ứng dụng m msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} đã chuyển sang {host}. Mở trên trình duyệt để cài đặt ứng dụng mới." +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} hiện đã có tại {host}. Hãy cài đặt từ đó, sau đó đăng nhập bằng ứng dụng này." + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} hiện đã có tại {host}. Hãy cài đặt từ đó." + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} cần quyền truy cập micrô để liệt kê thiết b msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} cần quyền truy cập ghi màn hình. Hãy cho phép ghi màn hình trong cài đặt quyền riêng tư của hệ điều hành và khởi động lại ứng dụng." -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} hiện đã có tại {host}. Hãy cài đặt từ đó, sau đó đăng nhập bằng ứng dụng này." - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} giờ đã có tại {host}. Hãy cài đặt từ đó." - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "Có cuộc gọi đang diễn ra" msgid "Call confirmation" msgstr "Xác nhận cuộc gọi" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "Cuộc gọi đang ở cửa sổ riêng" @@ -14361,6 +14361,11 @@ msgstr "Nhập tên của bạn" msgid "Enter your password." msgstr "Nhập mật khẩu của bạn." +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "Nhập mã thiết lập của bạn" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "Nhập tên người dùng và khóa khôi phục từ bộ dụng cụ khôi phục của bạn, sau đó chọn mật khẩu mới." @@ -19448,7 +19453,7 @@ msgstr "Giá cũ" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "Cho phép /tts đọc to tin nhắn của bạn. Tắt cài đặt này sẽ giữ các lệnh đó dưới dạng văn bản thông thường." -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "Cho phép bot trong cộng đồng này nhắn tin trực tiếp cho bạn" @@ -25279,7 +25284,7 @@ msgstr "Gói dịch vụ" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "Các gói, đặc quyền, quà tặng và gói đăng ký của bạn đều nằm trên một trang." #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "Mở người dùng ở cửa sổ riêng" msgid "Pop out video" msgstr "Mở video ở cửa sổ riêng" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "Đang ở cửa sổ riêng" @@ -30120,6 +30125,11 @@ msgstr "Cài đặt." msgid "Setup key" msgstr "Khóa thiết lập" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "Mã thiết lập" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "Thẻ #0000 được dành riêng cho người đăng ký Visionary. Vis msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API đang ở một nguồn gốc khác với trang này, nên các yêu cầu thiết lập được gửi đi mà không có phiên của bạn. Hãy kiểm tra nguồn gốc và cổng công khai mà máy chủ này được cấu hình, sau đó tải lại." +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API đã ghi mã thiết lập vào nhật ký trong lần khởi động đầu tiên. Nếu bạn đã đặt FLUXER_SETUP_TOKEN, hãy dùng giá trị đó." + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "Ứng dụng đã gặp sự cố" @@ -33685,6 +33700,11 @@ msgstr "Hệ thống đã cấp cho {target} vai trò {role}" msgid "The system removed the role {role} from {target}" msgstr "Hệ thống đã gỡ bỏ vai trò {role} khỏi {target}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "Mã thiết lập chứng minh bạn đang vận hành phiên bản này. Chỉ người có mã này mới có thể tạo tài khoản quản trị viên." + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "Bên thứ ba" msgid "This account can't be blocked." msgstr "Không thể chặn tài khoản này." +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "Tài khoản này không phải là quản trị viên của phiên bản này. Hãy đăng nhập bằng tài khoản quản trị viên để hoàn tất thiết lập." + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/zh-CN/messages.po b/fluxer_app/src/features/i18n/locales/zh-CN/messages.po index 9f27f57d5..efd802110 100644 --- a/fluxer_app/src/features/i18n/locales/zh-CN/messages.po +++ b/fluxer_app/src/features/i18n/locales/zh-CN/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} 已迁移到 {host}。安装新应用后,你将自动登 msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} 已迁移到 {host}。请在浏览器中打开以安装新应用。" +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} 现已迁移至 {host}。请从新地址安装,然后使用此应用登录。" + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} 现已迁移至 {host}。请从新地址安装。" + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} 需要麦克风权限才能列出你的设备。" msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} 需要屏幕录制权限。请在操作系统的隐私设置中允许屏幕录制,然后重启应用。" -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} 现已迁移至 {host}。请从新地址安装,然后使用此应用登录。" - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} 现已迁移至 {host}。请从新地址安装。" - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "可加入通话" msgid "Call confirmation" msgstr "通话确认" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "通话已弹出" @@ -14361,6 +14361,11 @@ msgstr "输入你的名字" msgid "Enter your password." msgstr "请输入你的密码。" +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "输入你的设置令牌" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "输入您的用户名和恢复工具包中的恢复密钥,然后选择一个新密码。" @@ -19448,7 +19453,7 @@ msgstr "旧版费率" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "让 /tts 朗读你的消息。禁用此设置后,这些命令将显示为普通文本。" -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "允许此社区中的机器人私信你" @@ -25279,8 +25284,8 @@ msgstr "套餐" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." -msgstr "方案、福利、礼物和你的订阅都在一个页面上。" +msgid "Plans, perks, gifts and your subscription are all on one page." +msgstr "套餐、福利、礼品和您的订阅都在一个页面上。" #: src/features/auth/flow/HandoffApprovalFlow.tsx msgid "Platform" @@ -25403,7 +25408,7 @@ msgstr "弹出用户" msgid "Pop out video" msgstr "弹出视频" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "已弹出" @@ -30120,6 +30125,11 @@ msgstr "设置。" msgid "Setup key" msgstr "设置密钥" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "设置令牌" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "#0000 标签为 Visionary 订阅者保留。Visionary 已无法再购买 msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API 与此页面不同源,因此设置请求不会带上你的会话。请检查此实例配置的公开源地址和端口,然后重新加载。" +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API 首次启动时将令牌写入了日志。如果您设置了 FLUXER_SETUP_TOKEN,请使用该值。" + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "应用崩溃了" @@ -33685,6 +33700,11 @@ msgstr "系统已将身份组 {role} 授予 {target}" msgid "The system removed the role {role} from {target}" msgstr "系统已移除 {target} 的身份组 {role}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "该令牌证明您运行的是此实例。只有拥有该令牌的人才能创建管理员帐户。" + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "第三方" msgid "This account can't be blocked." msgstr "此账号无法被屏蔽。" +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "此账号不是此实例的管理员。请使用管理员账号登录以完成设置。" + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/i18n/locales/zh-TW/messages.po b/fluxer_app/src/features/i18n/locales/zh-TW/messages.po index 156296bea..3273318b8 100644 --- a/fluxer_app/src/features/i18n/locales/zh-TW/messages.po +++ b/fluxer_app/src/features/i18n/locales/zh-TW/messages.po @@ -1911,6 +1911,16 @@ msgstr "{productName} 已移至 {host}。安裝新應用程式後,您將自動 msgid "{productName} has moved to {host}. Open it in your browser to install the new app." msgstr "{productName} 已移至 {host}。請在瀏覽器中開啟以安裝新應用程式。" +#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there, then sign in with this app." +msgstr "{productName} 現在位於 {host}。請從該處安裝,然後使用此應用程式登入。" + +#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. +#: src/features/app/domain_migration/DomainMovedStepsModal.tsx +msgid "{productName} is now at {host}. Install it from there." +msgstr "{productName} 現在位於 {host}。請從該處安裝。" + #. Profile membership label showing when the user account was created. #: src/features/user/components/popouts/UserProfileShared.tsx msgid "{productName} member since" @@ -1951,16 +1961,6 @@ msgstr "{productName} 需要麥克風存取權限才能列出您的裝置。" msgid "{productName} needs screen recording access. Allow screen recording in your operating system privacy settings and restart the app." msgstr "{productName} 需要螢幕錄影權限。請在您的作業系統隱私設定中允許螢幕錄影,然後重新啟動應用程式。" -#. Intro in the modal that explains how to install the app from its new domain. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there, then sign in with this app." -msgstr "{productName} 現在已移至 {host}。請從該處安裝,然後使用此應用程式登入。" - -#. Intro in the modal that explains how to install the app from its new domain when the account already moved. productName is the app name. host is the new domain, for example fluxer.com. -#: src/features/app/domain_migration/DomainMovedStepsModal.tsx -msgid "{productName} now lives at {host}. Install it from there." -msgstr "{productName} 現在已移至 {host}。請從該處安裝。" - #. Short badge title in the user profile badges popout. Preserve {productName}; it is inserted by code. English locales use Title Case for official badge titles; other locales should use natural local capitalization. #: src/features/user/components/popouts/UserProfileBadges.tsx msgid "{productName} Partner" @@ -6365,7 +6365,7 @@ msgstr "可加入通話" msgid "Call confirmation" msgstr "通話確認" -#. Overlay text shown in the in-app call area while the call view lives in a separate popped-out window. +#. Overlay text shown in the in-app call area while the call view is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Call is popped out" msgstr "通話已在獨立視窗開啟" @@ -14361,6 +14361,11 @@ msgstr "輸入您的名字" msgid "Enter your password." msgstr "請輸入您的密碼。" +#. Setup wizard title for the step that asks the operator for the setup token of a new instance. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Enter your setup token" +msgstr "輸入你的設定權杖" + #: src/features/auth/components/pages/RecoverAccountPage.tsx msgid "Enter your username and the recovery key from your recovery kit, then choose a new password." msgstr "輸入你的使用者名稱和復原套件中的復原金鑰,然後選擇一個新密碼。" @@ -19448,7 +19453,7 @@ msgstr "舊版費率" msgid "Let /tts read your message aloud. Disabling the setting keeps those commands as regular text." msgstr "讓 /tts 唸出您的訊息。關閉此設定會讓這些指令顯示為一般文字。" -#. Toggle label in the community privacy modal: allow DMs from bots that live in this community. +#. Toggle label in the community privacy modal: allow DMs from bots that are in this community. #: src/features/guild/components/modals/GuildPrivacySettingsModal.tsx msgid "Let bots in this community DM you" msgstr "允許此社群中的機器人傳送私訊給您" @@ -25279,7 +25284,7 @@ msgstr "方案" #. Text on the card in settings that links to the Plutonium page. #: src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx -msgid "Plans, perks, gifts and your subscription all live on one page." +msgid "Plans, perks, gifts and your subscription are all on one page." msgstr "方案、福利、禮物和你的訂閱都在同一個頁面上。" #: src/features/auth/flow/HandoffApprovalFlow.tsx @@ -25403,7 +25408,7 @@ msgstr "以獨立視窗開啟使用者" msgid "Pop out video" msgstr "以獨立視窗開啟視訊" -#. Overlay text shown on a voice participant tile while it lives in a separate popped-out window. +#. Overlay text shown on a voice participant tile while it is in a separate popped-out window. #: src/features/voice/components/popout/PoppedOutOverlay.tsx msgid "Popped out" msgstr "已在獨立視窗開啟" @@ -30120,6 +30125,11 @@ msgstr "設定。" msgid "Setup key" msgstr "設定金鑰" +#. Label for the setup token input in the setup wizard. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "Setup token" +msgstr "設定權杖" + #. Label in the channel and chat typing users. #: src/features/typing/utils/TypingTierText.ts msgid "Several people are typing..." @@ -33428,6 +33438,11 @@ msgstr "#0000 標籤保留給 Visionary 訂閱者。Visionary 已停止販售。 msgid "The API is on a different origin than this page, so setup requests are sent without your session. Check the public origin and port this instance is configured with, then reload." msgstr "API 的網域與此頁面不同,因此設定要求不會帶有您的工作階段。請檢查此實例設定的公開網域和埠號,然後重新載入。" +#. Help text under the setup token input in the setup wizard. FLUXER_SETUP_TOKEN is an environment variable name and must not be translated. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The API wrote the token to its log the first time it started. If you set FLUXER_SETUP_TOKEN, use that value." +msgstr "API 在首次啟動時將權杖寫入其日誌。如果你設定了 FLUXER_SETUP_TOKEN,請使用該值。" + #: src/features/app/components/ErrorFallback.tsx msgid "The app crashed" msgstr "應用程式當機了" @@ -33685,6 +33700,11 @@ msgstr "系統已將身分組 {role} 授予 {target}" msgid "The system removed the role {role} from {target}" msgstr "系統已移除 {target} 的身分組 {role}" +#. Setup wizard body explaining why the operator has to enter the setup token. +#: src/features/app/components/setup/SetupWizardSteps.tsx +msgid "The token proves that you run this instance. Only the person who has it can create the admin account." +msgstr "這個權杖證明此執行個體由你管理。只有持有權杖的人才能建立管理員帳號。" + #. Native desktop dialog body under the headline saying the update did not finish. The buttons are Try again, Download and Later. #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "The update did not download or install. Try again or download it yourself." @@ -33903,6 +33923,11 @@ msgstr "第三方" msgid "This account can't be blocked." msgstr "無法封鎖此帳號。" +#. Error shown in the setup wizard when the signed-in account lacks admin access to the unconfigured instance. A sign out button follows. +#: src/features/app/components/setup/SelfHostedSetupWizardGate.tsx +msgid "This account is not an admin of this instance. Sign in with an admin account to finish setup." +msgstr "此帳號不是此執行個體的管理員。請使用管理員帳號登入以完成設定。" + #. Error in the browser when the chosen account belongs to a different server than the app that asked to sign in. accountHost and requestHost are server addresses. productName is the app name, such as Fluxer. #: src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts msgid "This account is on {accountHost}, but the {productName} app is signing in to {requestHost}. Choose an account on {requestHost}." diff --git a/fluxer_app/src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx b/fluxer_app/src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx index 6fbf89f1f..45764de51 100644 --- a/fluxer_app/src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx +++ b/fluxer_app/src/features/premium/components/plutonium_page/PlutoniumPageLinkCard.tsx @@ -9,7 +9,7 @@ import {useLingui} from '@lingui/react/macro'; import {CrownIcon} from '@phosphor-icons/react'; const LINK_CARD_BODY_DESCRIPTOR = msg({ - message: 'Plans, perks, gifts and your subscription all live on one page.', + message: 'Plans, perks, gifts and your subscription are all on one page.', comment: 'Text on the card in settings that links to the Plutonium page.', }); const LINK_CARD_BUTTON_DESCRIPTOR = msg({ diff --git a/fluxer_app/src/features/voice/components/popout/PoppedOutOverlay.tsx b/fluxer_app/src/features/voice/components/popout/PoppedOutOverlay.tsx index fb0772d22..5e616e5e7 100644 --- a/fluxer_app/src/features/voice/components/popout/PoppedOutOverlay.tsx +++ b/fluxer_app/src/features/voice/components/popout/PoppedOutOverlay.tsx @@ -15,11 +15,11 @@ import {useCallback} from 'react'; const CALL_POPPED_OUT_DESCRIPTOR = msg({ message: 'Call is popped out', - comment: 'Overlay text shown in the in-app call area while the call view lives in a separate popped-out window.', + comment: 'Overlay text shown in the in-app call area while the call view is in a separate popped-out window.', }); const TILE_POPPED_OUT_DESCRIPTOR = msg({ message: 'Popped out', - comment: 'Overlay text shown on a voice participant tile while it lives in a separate popped-out window.', + comment: 'Overlay text shown on a voice participant tile while it is in a separate popped-out window.', }); const FOCUS_WINDOW_DESCRIPTOR = msg({ diff --git a/fluxer_desktop/src/main/BootstrapEntry.test.mjs b/fluxer_desktop/src/main/BootstrapEntry.test.mjs index 0a53777ca..c68825faf 100644 --- a/fluxer_desktop/src/main/BootstrapEntry.test.mjs +++ b/fluxer_desktop/src/main/BootstrapEntry.test.mjs @@ -222,7 +222,7 @@ describe('Bootstrap entry point', () => { assert.doesNotMatch( source, /import\('@electron\/main\/Autostart'\)/, - 'Autostart.ts pulls the main process i18n and the Linux portals in. The bootstrap only needs the launch check, which lives in the light AutostartLaunch module.', + 'Autostart.ts pulls the main process i18n and the Linux portals in. The bootstrap only needs the launch check, which is in the light AutostartLaunch module.', ); }); diff --git a/fluxer_desktop/src/main/ModuleStore.test.mjs b/fluxer_desktop/src/main/ModuleStore.test.mjs index 456a217ec..c0d8c0546 100644 --- a/fluxer_desktop/src/main/ModuleStore.test.mjs +++ b/fluxer_desktop/src/main/ModuleStore.test.mjs @@ -207,7 +207,7 @@ const RENDERER_BAD_MEMBER = packModule('fluxer_renderer', [['assets/index-v1.js' }); describe('ModuleStore', () => { - test('lives beside the account store, never inside it', () => { + test('is beside the account store, never inside it', () => { const userDataPath = createUserData(); const root = getModuleStoreRoot(userDataPath); assert.equal(root, path.join(userDataPath, 'modules')); diff --git a/fluxer_desktop/src/main/SplashBundleOwnership.test.mjs b/fluxer_desktop/src/main/SplashBundleOwnership.test.mjs index dd8476c46..577d44308 100644 --- a/fluxer_desktop/src/main/SplashBundleOwnership.test.mjs +++ b/fluxer_desktop/src/main/SplashBundleOwnership.test.mjs @@ -100,7 +100,7 @@ describe('splash bundle ownership', () => { assert.equal( edges.has(SPLASH_MODULE), false, - 'Updater.ts imports this module into the main app bundle, so a splash import here drags the whole splash across the bundle boundary. The presentation lives in ShellUpdateSplash.ts.', + 'Updater.ts imports this module into the main app bundle, so a splash import here drags the whole splash across the bundle boundary. The presentation is in ShellUpdateSplash.ts.', ); }); }); diff --git a/fluxer_docs/src/content/docs/admin-api/index.mdx b/fluxer_docs/src/content/docs/admin-api/index.mdx index c14e91d2e..5e59e4dd0 100644 --- a/fluxer_docs/src/content/docs/admin-api/index.mdx +++ b/fluxer_docs/src/content/docs/admin-api/index.mdx @@ -28,8 +28,6 @@ No Admin operation declares an OAuth2 scope, an MFA requirement, or a sudo requi An Admin API key authenticates as the account that created it, and Fluxer reads it only on a path below `/v1/admin`. A request presenting a key on any other path is treated as a request with no credential. Fluxer authorises a key-authenticated request twice. The key's own ACL set must satisfy the operation, and unless the owning account holds `*` that account must itself hold one of the ACLs the operation names. When an ACL is removed from a key, or from the account that owns the key, the key no longer passes a check for that ACL on its next request. The key does not need to be rotated. -Initial setup is the single exception. While `app_public.setup.configured` is false, a session credential reaches the [instance configuration](/admin-api/instance/) operations without holding any ACL. The session that switches setup to configured is granted `*` immediately. After `app_public.setup.configured` becomes true, those operations require an accepted credential and the ACLs they name. - Admin authorisation runs before request validation on every operation except [Queue bulk job](/admin-api/bulk-jobs/#queue-bulk-job), whose body is validated first because its ACL is selected from that body. An account lacking the required ACL is therefore refused with 403 even when its query string or body is also malformed. ## ACL evaluation diff --git a/fluxer_docs/src/content/docs/admin-api/instance.mdx b/fluxer_docs/src/content/docs/admin-api/instance.mdx index 51db61604..16edf47db 100644 --- a/fluxer_docs/src/content/docs/admin-api/instance.mdx +++ b/fluxer_docs/src/content/docs/admin-api/instance.mdx @@ -12,12 +12,6 @@ Every write is a merge over the stored configuration, and an omitted key leaves Reading configuration requires the [Admin ACL](/admin-api/#acl-registry) `instance:config:view`, and writing it requires `instance:config:update`. The limit configuration has its own pair, `instance:limit_config:view` and `instance:limit_config:update`. -:::note[Initial setup accepts a plain session credential] -Until setup is marked complete, [Get instance configuration](#get-instance-configuration), [Update instance configuration](#update-instance-configuration), [Create branding asset](#create-branding-asset), and [Create SMTP test](#create-smtp-test) also accept a session credential holding no Admin ACL. -::: - -That relaxation applies only to a session credential, so an Admin API key and a bearer token are evaluated normally even before setup is complete. Completing setup grants the acting session the wildcard ACL. Registration URLs, pending registrations, and limit configuration always need their own ACL. - :::caution[No read returns a stored secret] A secret is reported by a companion boolean such as `client_secret_set`. Except for the single sign-on client secret, that boolean is true whether the value is stored here or supplied by deployment configuration. ::: @@ -627,7 +621,7 @@ Display metadata for one [limit key](/http-api/instance/#limit-keys), which an e -Returns the [instance configuration](#instance-configuration-object) object. Requires `instance:config:view`, or a session credential until setup is marked complete. +Returns the [instance configuration](#instance-configuration-object) object. Requires `instance:config:view`. ### Response @@ -637,7 +631,7 @@ Returns the [instance configuration](#instance-configuration-object) object. Req ### Side effects -The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `get_instance_config`, target type `instance_config`, target ID `0`, and metadata keys `registration_url_count` and `pending_registration_count`. Before setup is complete, a session credential holding no Admin ACL records the entry with its own account as the acting Admin. +The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `get_instance_config`, target type `instance_config`, target ID `0`, and metadata keys `registration_url_count` and `pending_registration_count`. ### Rate limit @@ -647,7 +641,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje -Applies a merge patch to the stored configuration and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`, or a session credential until setup is marked complete. +Applies a merge patch to the stored configuration and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`. :::note[Every section is optional and merged independently] The body has one optional object for each section. Fluxer leaves an absent section alone. Within a section, an absent key keeps its stored value, and a key sent as null clears the stored value where the schema permits null. @@ -728,9 +722,9 @@ The order is `gateway_rollout`, `push_relay`, `domain_migration`, `voice_p2p`, ` Fluxer publishes a `gateway_rollout` change to the Gateway cluster. A `push_relay` change reaches the push service without a restart. A `billing` change applies without a restart. Premium mode changes affect the limits in force without replacing the saved limit configuration. On self-hosted deployments, `everyone` hides premium-filtered rules. Switching back to `mirror` restores them unless an Admin has replaced the limit configuration in the meantime. Enabling single community mode creates the community when none is designated, with the acting Admin as owner. -Initial setup completes on the first update that sets `app_public.setup.configured` to true from a session credential whose account holds neither `admin:authenticate` nor the wildcard. That update grants the account the wildcard Admin ACL and marks the deployment as bootstrapped. +An update that sets `app_public.setup.configured` to true also marks the deployment as bootstrapped, so no later registration receives the wildcard Admin ACL. -The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_instance_config`, target type `instance_config`, and target ID `0`. The metadata key `sections` lists the names of the non-null top-level sections in the body, comma-separated in alphabetical order, and is absent when the body supplies none. The update that completes initial setup and grants the wildcard ACL also records `granted_acls` as `*`. No value from the body is recorded. A request that fails part way records no entry. +The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_instance_config`, target type `instance_config`, and target ID `0`. The metadata key `sections` lists the names of the non-null top-level sections in the body, comma-separated in alphabetical order, and is absent when the body supplies none. No value from the body is recorded. A request that fails part way records no entry. ### Rate limit @@ -740,7 +734,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje -Uploads an image, stores its [Media Proxy](/media-proxy/overview/) URL in the selected branding slot, and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`, or a session credential until setup is marked complete. +Uploads an image, stores its [Media Proxy](/media-proxy/overview/) URL in the selected branding slot, and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`. ### JSON body @@ -776,7 +770,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje -Opens a connection to an SMTP server, authenticates against it, and reports the outcome as an [SMTP test result](#smtp-test-result-object) object without storing anything. Requires `instance:config:update`, or a session credential until setup is marked complete. +Opens a connection to an SMTP server, authenticates against it, and reports the outcome as an [SMTP test result](#smtp-test-result-object) object without storing anything. Requires `instance:config:update`. :::note[A rejected credential is still a successful test] A test that runs to completion answers 200 whether or not the server accepted the connection. The `ok` field is the verdict and `error` is the failure text. diff --git a/fluxer_docs/src/content/docs/admin-api/messages.mdx b/fluxer_docs/src/content/docs/admin-api/messages.mdx index bd5b9f7cc..9cb6a136d 100644 --- a/fluxer_docs/src/content/docs/admin-api/messages.mdx +++ b/fluxer_docs/src/content/docs/admin-api/messages.mdx @@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro'; Admin message routes read, search, and permanently delete the messages of one channel. Another reports the progress of a shred job. -Shredding and bulk deletion for a whole account live under [Admin users](/admin-api/users/). +Shredding and bulk deletion for a whole account are under [Admin users](/admin-api/users/). ## Admin message object diff --git a/fluxer_docs/src/content/docs/http-api/authentication.mdx b/fluxer_docs/src/content/docs/http-api/authentication.mdx index de19c3388..1072d5779 100644 --- a/fluxer_docs/src/content/docs/http-api/authentication.mdx +++ b/fluxer_docs/src/content/docs/http-api/authentication.mdx @@ -572,6 +572,7 @@ Registration verifies a [CAPTCHA](/topics/captcha/). It permits 3 attempts per h | invite_code? | ?string | The invite accepted immediately after registration (0-256 characters) | | registration_url_code?6 | ?string | The administrator-issued registration URL code (1-256 characters) | | theme? | string | The initial theme preference, one of `dark`, `dark_legacy`, `coal`, `light`, or `system` | +| setup_token?7 | ?string | The setup token of a new self-hosted instance (1-256 characters) | 1 Omitting the email address creates an unclaimed account, which has no recovery path until it is claimed. An address whose domain has no usable DNS records, or whose top-level domain the instance blocks, returns the field code `INVALID_EMAIL_ADDRESS`, and an address already in use returns `EMAIL_ALREADY_IN_USE`. A `username` instance accepts any string of 1-320 characters here, because older apps send one, and discards it. It stores no address and sends no email. It does not check the address against DNS or the blocklists, or whether another account uses it @@ -585,10 +586,14 @@ Registration verifies a [CAPTCHA](/topics/captcha/). It permits 3 attempts per h 6 A code supplied on an instance with administrator registration URLs disabled, and a code that does not resolve, both return 400 `REGISTRATION_URL_INVALID`. A valid code overrides the closed registration mode and replaces the instance approval mode with its own approval setting +7 Required while a self-hosted instance is unconfigured and has no admin account, where an absent or blank value returns the field code `SETUP_TOKEN_REQUIRED`. Accepted until the first admin account exists, and a wrong value returns the field code `SETUP_TOKEN_INVALID`. Ignored afterwards, on a hosted instance, and on a self-hosted instance that does not [enforce the token](/operator/configuration/#setup-token). Surrounding whitespace is trimmed + Fluxer resolves the region from the client IP address, and an age below the minimum for that region returns the field code `MUST_BE_MINIMUM_AGE`. That minimum is 13 years unless the region sets a different minimum, and the applied minimum appears only in the localised message. Registration returns 403 `REGISTRATION_CLOSED` when the instance is closed and no valid registration URL was supplied. A username whose discriminator space is exhausted returns the field code `TOO_MANY_USERS_WITH_THIS_USERNAME`. A username or display name containing a blocked substring returns 403 `CONTENT_BLOCKED`. +After the route rate limit, the CAPTCHA and schema validation, the setup token is checked before every other registration rule. A registration that presents the correct token is admitted whatever the registration mode is, is exempt from the registration allowances above, and receives the wildcard [Admin ACL](/admin-api/#acl-registry) when it creates a claimed account, which needs `email` on an `email` instance and `password` on a `username` instance. A client IP address is permitted 10 wrong tokens per 30 minutes, and further requests that present a token from that address return 429 until the window passes, the correct token included. A request with no token does not count. + :::caution[Registration reports an address already in use] [Log in with a password](#log-in-with-a-password) and [request password recovery](#request-password-recovery) are enumeration-safe and registration is not. An address that already holds an account returns the field code `EMAIL_ALREADY_IN_USE`, so an unauthenticated caller learns from the response whether that address is registered. ::: @@ -602,10 +607,10 @@ There is no retry key. A repeated request with the same values creates a second | Status | Body | Condition | | --- | --- | --- | | 200 | [authentication token response](#authentication-token-response) \| [registration pending approval response](#registration-pending-approval-response-object) | Registration completed or entered the approval state | -| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, account fields, or registration URL code is invalid | +| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, account fields, registration URL code, or setup token is invalid | | 403 | [error response](/http-api/#error-response) | SSO enforcement, registration policy, or content moderation rejects the registration | | 423 | [error response](/http-api/#error-response) | `GENERAL_ERROR`, because a self-hosted instance with no account yet is still settling its [sign-in method](/http-api/instance/#set-the-setup-sign-in-method) after 15 seconds | -| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, email, IP, or subnet bucket denies the request | +| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, email, IP, or subnet bucket denies the request, or the address has used its wrong setup token allowance | | 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs | | 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling | diff --git a/fluxer_docs/src/content/docs/http-api/errors.md b/fluxer_docs/src/content/docs/http-api/errors.md index 437118cb4..57761a90f 100644 --- a/fluxer_docs/src/content/docs/http-api/errors.md +++ b/fluxer_docs/src/content/docs/http-api/errors.md @@ -2003,6 +2003,14 @@ Referenced attachment "{filename}" wasn't found in message attachments Session timed out +### `SETUP_TOKEN_INVALID` + +That setup token is not correct. Check the token and try again. + +### `SETUP_TOKEN_REQUIRED` + +This instance is not set up yet. Finish setup in a web browser with the setup token before creating accounts. + ### `SNOWFLAKE_OUT_OF_RANGE` Invalid snowflake diff --git a/fluxer_docs/src/content/docs/http-api/guild-members.mdx b/fluxer_docs/src/content/docs/http-api/guild-members.mdx index 57a73aa03..52e084a26 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-members.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-members.mdx @@ -6,7 +6,7 @@ description: Guild member objects, profiles, voice moderation, ownership, remova import RouteHeader from '@/components/RouteHeader.astro'; -A guild member is an account that has joined a guild. The membership has a nickname, avatar, role set, and moderation state that apply in that guild alone. Indexed member queries live on [Guild member search](/http-api/guild-member-search/), and bans on [Guild moderation](/http-api/guild-moderation/). +A guild member is an account that has joined a guild. The membership has a nickname, avatar, role set, and moderation state that apply in that guild alone. Indexed member queries are on [Guild member search](/http-api/guild-member-search/), and bans on [Guild moderation](/http-api/guild-moderation/). An unknown guild returns 404 `UNKNOWN_GUILD`, a non-member receives 403 `MISSING_PERMISSIONS`, and a guild marked unavailable returns 403 `MISSING_ACCESS`. [Transfer guild ownership](#transfer-guild-ownership) also returns 403 `ACCESS_DENIED` when the guild exists in storage but the Gateway reports it as not found. diff --git a/fluxer_docs/src/content/docs/http-api/guilds.mdx b/fluxer_docs/src/content/docs/http-api/guilds.mdx index 9d06e2ffe..0282d79df 100644 --- a/fluxer_docs/src/content/docs/http-api/guilds.mdx +++ b/fluxer_docs/src/content/docs/http-api/guilds.mdx @@ -6,7 +6,7 @@ description: Guild objects, creation templates, configuration registries, and li import RouteHeader from '@/components/RouteHeader.astro'; -A guild is a community with its own channels, roles, members, and configuration. Membership and ownership transfer live on [Guild members](/http-api/guild-members/), and channels on [Guild channels](/http-api/guild-channels/). +A guild is a community with its own channels, roles, members, and configuration. Membership and ownership transfer are on [Guild members](/http-api/guild-members/), and channels on [Guild channels](/http-api/guild-channels/). [List current user guilds](#list-current-user-guilds) and [Get guild](#get-guild) declare the `guilds` [OAuth2 scope](/http-api/oauth2/#oauth2-scopes), and a bearer credential without that scope receives 403 `MISSING_OAUTH_SCOPE`. Every other route rejects a bearer credential with 403 `ACCESS_DENIED`. diff --git a/fluxer_docs/src/content/docs/http-api/instance.mdx b/fluxer_docs/src/content/docs/http-api/instance.mdx index dcf0a005d..2b07802c3 100644 --- a/fluxer_docs/src/content/docs/http-api/instance.mdx +++ b/fluxer_docs/src/content/docs/http-api/instance.mdx @@ -444,11 +444,14 @@ Whether the deployment has finished its initial configuration, and where an oper | configured | boolean | Whether initial deployment configuration is complete | | admin_url1 | ?string | The admin application URL used to continue setup, or null | | account_identity_locked? | boolean | Whether the sign-in method can no longer change, sent only while a self-hosted deployment is not configured | +| setup_token_required? | boolean | Whether registration needs the setup token, sent only while a self-hosted deployment is not configured | 1 The value is the deployment's configured admin endpoint, and it is null when no admin endpoint is configured `account_identity_locked` is true once the first account exists, and on a deployment stored as already in use. [Set the setup sign-in method](#set-the-setup-sign-in-method) returns 409 there. +`setup_token_required` is true while [Register an account](/http-api/authentication/#register-an-account) refuses a body without `setup_token`. It is false once the first admin account exists, and on a deployment that does not enforce the token. + ## Public legal configuration object The public document URLs a client links from its registration form and its report flows. @@ -615,8 +618,8 @@ Chooses how members sign in on a new self-hosted deployment, and on an `email` d The setup wizard calls this before the owner account exists. The choice stays open on a self-hosted deployment until setup is complete or the first account exists. After that, on a deployment that was already in use before username sign-in existed, and on every hosted deployment, the route returns 409 `ACCOUNT_IDENTITY_LOCKED`. The choice and the first registration take turns, so a choice never lands after the first account exists. A repeated call while the choice is open replaces the earlier one. -:::caution[Anyone can call this before the first account exists] -The route needs no credential, the same as the rule that makes the first registration the instance admin. An operator finishes setup in the same sitting that brings the deployment up. +:::caution[The setup token stands in for a credential] +The route needs no credential. While `setup_token_required` is true in the [public setup state](#public-setup-state-object), a call without the correct `setup_token` is refused. ::: ### JSON body @@ -625,18 +628,21 @@ The route needs no credential, the same as the rule that makes the first registr | --- | --- | --- | | mode | string | The [account identity mode](#account-identity-modes) to use | | tag_style?1 | string | How usernames are tagged, one of the [tag styles](#tag-styles). Defaults to `none` | +| setup_token?2 | ?string | The setup token of the deployment (1-256 characters) | 1 A `username` instance always uses `none`. Sending `random` with `username` returns the field code `TAG_STYLE_REQUIRES_EMAIL_SIGN_IN` on `tag_style` +2 Checked the way [Register an account](/http-api/authentication/#register-an-account) checks it, with the same field codes and the same allowance for wrong tokens + ### Response | Status | Body | Condition | | --- | --- | --- | | 200 | [account identity](#account-identity-object) object | The mode was stored | -| 400 | [error response](/http-api/#error-response) | The body is invalid, or `random` tags were requested with `username` | +| 400 | [error response](/http-api/#error-response) | The body is invalid, `random` tags were requested with `username`, or the setup token is missing or wrong | | 409 | [error response](/http-api/#error-response) | `ACCOUNT_IDENTITY_LOCKED`, because the sign-in method can no longer change | | 423 | [error response](/http-api/#error-response) | `GENERAL_ERROR`, because a registration or another choice held the turn for more than 15 seconds | -| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request | +| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request, or the address has used its wrong setup token allowance | ### Side effects diff --git a/fluxer_docs/src/content/docs/http-api/users.mdx b/fluxer_docs/src/content/docs/http-api/users.mdx index 0f484357b..b411f00ae 100644 --- a/fluxer_docs/src/content/docs/http-api/users.mdx +++ b/fluxer_docs/src/content/docs/http-api/users.mdx @@ -6,7 +6,7 @@ description: User objects, public profiles, account tags, and the settings recor import RouteHeader from '@/components/RouteHeader.astro'; -A user is a Fluxer account. Another account sees the [partial user object](#partial-user-object), and the owner sees the [user object](#user-object). Account mutation lives on [Current user](/http-api/users/current-user/). +A user is a Fluxer account. Another account sees the [partial user object](#partial-user-object), and the owner sees the [user object](#user-object). Account mutation is on [Current user](/http-api/users/current-user/). Every route on this page takes a user session token or a bot token. An OAuth2 bearer credential is rejected with 403 `ACCESS_DENIED`. diff --git a/fluxer_docs/src/content/docs/http-api/users/content.mdx b/fluxer_docs/src/content/docs/http-api/users/content.mdx index fbe3b60b2..e273e504c 100644 --- a/fluxer_docs/src/content/docs/http-api/users/content.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/content.mdx @@ -6,7 +6,7 @@ description: Recent mentions, saved messages, and deletion of the caller's own m import RouteHeader from '@/components/RouteHeader.astro'; -Recent mentions and saved messages are private lists that only the account owning them can read. Further routes delete the messages the caller authored, one filtered and immediate, the other unfiltered and delayed by a day. Data harvests live on [Data harvests](/http-api/users/data-harvest/). +Recent mentions and saved messages are private lists that only the account owning them can read. Further routes delete the messages the caller authored, one filtered and immediate, the other unfiltered and delayed by a day. Data harvests are on [Data harvests](/http-api/users/data-harvest/). These routes are user-only. Fluxer rejects a bot or OAuth2 bearer credential with 403 `ACCESS_DENIED`. diff --git a/fluxer_docs/src/content/docs/http-api/users/settings-protobuf.md b/fluxer_docs/src/content/docs/http-api/users/settings-protobuf.md index af1fcdbc5..d71aff032 100644 --- a/fluxer_docs/src/content/docs/http-api/users/settings-protobuf.md +++ b/fluxer_docs/src/content/docs/http-api/users/settings-protobuf.md @@ -86,7 +86,7 @@ An over-length string draws two entries for the one path. | double_tap_action | int32 | [Double tap action](#double-tap-actions) on a message | | swipe_right_to_left_action | int32 | [Swipe right to left action](#swipe-right-to-left-actions) in a channel | -1 The entries live inside the snapshot, and the account [memes](/http-api/memes/) collection holds none of them +1 The entries are inside the snapshot, and the account [memes](/http-api/memes/) collection holds none of them 2 The only bare scalar on the root message, so it has no presence and reads as false until a client stores true. It applies to the content a client is about to send, and the first-party client leaves the URLs inside inline code and code blocks unchanged diff --git a/fluxer_docs/src/content/docs/operator/configuration.mdx b/fluxer_docs/src/content/docs/operator/configuration.mdx index 080537eb5..ebdeb0564 100644 --- a/fluxer_docs/src/content/docs/operator/configuration.mdx +++ b/fluxer_docs/src/content/docs/operator/configuration.mdx @@ -18,7 +18,7 @@ The pass unlocks nothing in the software, and self-hosting stays free. If you ha We are grateful to everyone supporting the project through [Fluxer Plutonium](https://fluxer.app/plutonium) or [donations](https://fluxer.app/donate). All of our code is free and open source on [GitHub](https://github.com/fluxerapp/fluxer). ::: -Fluxer reads its settings from environment variables. They live in a file named `.env`, in the same directory as `docker-compose.yml`. +Fluxer reads its settings from environment variables. They are in a file named `.env`, in the same directory as `docker-compose.yml`. The installer in [Get started](/operator/get-started/) writes `.env` and fills in every secret. Start with [Core identity and public address](#core-identity-and-public-address) and [Secrets you must generate](#secrets-you-must-generate). @@ -1146,7 +1146,7 @@ Set by Docker. Node identity in metrics and logs. #### `FLUXER_ENV` -Default `development`. The runtime mode. `development`, `production`, or `test`. Compose pins `production`, which gates Postgres validation and the admin cookie flags. +Default `development`. The runtime mode. `development`, `production`, or `test`. Compose pins `production`, which gates Postgres validation and the admin cookie flags. On a self-hosted instance, `production` also enforces the [setup token](#setup-token). #### `FLUXER_SELF_HOSTED` @@ -1278,7 +1278,13 @@ Default empty. The incident history page of the status page. #### `FLUXER_INSTANCE_SETUP_CONFIGURED` -Default `false`. Whether setup is marked complete. The stored `app_public_config` row wins whenever its `setup.configured` is a boolean. This variable supplies the default only while no such row exists, and only on a self-hosted instance. Off a self-hosted instance the state is always true whatever this says. +Default `false`. Whether setup is marked complete. The stored `app_public_config` row wins whenever its `setup.configured` is a boolean. This variable supplies the default only while no such row exists, and only on a self-hosted instance. Off a self-hosted instance the state is always true whatever this says. Setting it to `true` on a new instance skips the setup wizard and opens registration, and nobody becomes admin by registering first. The admin account is then created by a [registration request](/http-api/authentication/#register-an-account) that includes `setup_token`. + + + +#### `FLUXER_SETUP_TOKEN` + +Default empty. The setup token of a new self-hosted instance, 32 to 256 characters, and a value outside that range fails startup. Until the first admin account exists, only the registration that presents this token becomes the admin. While the instance is also unconfigured, every registration must present it. Unset, the API generates a token on the first start, stores only its hash and writes the token to its log once. A value set here always replaces the generated one. It is read by the `api` service only and is ignored once the first admin exists. Off a self-hosted instance it is ignored. The installer writes one into `.env`, and [Create the first admin](/operator/get-started/#first-run) has where the wizard asks for it. #### `FLUXER_ACCOUNT_IDENTITY` @@ -1300,7 +1306,7 @@ Default empty. The guild that grants the visionary role. Snowflake. Default empty. The role granted there. Snowflake. -Completing setup saves that state independently of the environment default. Later branding or legal updates preserve it. Setting `FLUXER_INSTANCE_SETUP_CONFIGURED` back to `false` does not reopen the wizard or restore the unauthenticated setup access and first-registration admin grant described in [Get started](/operator/get-started/). +Completing setup saves that state independently of the environment default. Later branding or legal updates preserve it. Setting `FLUXER_INSTANCE_SETUP_CONFIGURED` back to `false` does not reopen the wizard or restore the admin grant described in [Create the first admin](/operator/get-started/#first-run). ## API and worker settings diff --git a/fluxer_docs/src/content/docs/operator/get-started.mdx b/fluxer_docs/src/content/docs/operator/get-started.mdx index 6bcdc3d5d..0a0f752ce 100644 --- a/fluxer_docs/src/content/docs/operator/get-started.mdx +++ b/fluxer_docs/src/content/docs/operator/get-started.mdx @@ -72,8 +72,8 @@ Choose the hostname the instance answers on. Step 4 writes it into `.env` as `FL Create an `A` record for that hostname pointing at the machine's public address, and an `AAAA` record when the machine has a public IPv6 address. A hosting provider shows that address in its console. On a home connection the address belongs to the router, and the router's status page shows it. -:::caution[Finish setup before anyone else reaches the hostname] -An unconfigured instance takes registrations from anyone, and lets any account that registers read and change the instance configuration. Registration defaults to `open`. The window opens the moment DNS and the ports are live, so finish the wizard in [Step 6](#first-run) in the same sitting that brings the stack up. +:::caution[Keep the setup token private] +An unconfigured instance creates no account without its setup token, and the registration that presents it becomes the admin. After that the token stops working and registration follows the registration mode, which defaults to `open`. ::: ## Step 3: Open the ports @@ -146,7 +146,7 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 -Domain chat.example.com The run prints one line per phase and ends with the URL to open. -`~/fluxer` then holds `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml`, `external-object-store.compose.yml`, `Caddyfile` and `.env.example`, plus a `.env` readable only by you. Every value that ships as `CHANGE_ME` in `.env.example` is a fresh random value. Every command from here on runs in that directory. +`~/fluxer` then holds `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml`, `external-object-store.compose.yml`, `Caddyfile` and `.env.example`, plus a `.env` readable only by you. Every value that ships as `CHANGE_ME` in `.env.example` is a fresh random value. The installer also writes `FLUXER_SETUP_TOKEN`, the setup token that [Step 6](#first-run) asks for. Every command from here on runs in that directory. ### Serving on another port @@ -226,11 +226,24 @@ Every path returns 200. `/.well-known/fluxer` lists the endpoints clients use, w Open the hostname in a browser. A fresh instance is unconfigured, so it serves the setup wizard. -The wizard first asks how members sign in. Usernames is the default and needs no email setup at all. Email suits an instance that sends mail through SMTP. The choice is fixed once the owner account exists. [Sign-in methods](/operator/sign-in/) explains both. +After the welcome screen the wizard asks for the setup token. The token proves that you run the instance, and nobody can create an account on an unconfigured instance without it. -Then create the owner account, and choose branding, registration mode, community policy, media expiry, integrations and premium settings. Completing the wizard closes setup access. +- The installer prints the token as its last step, and keeps it as `FLUXER_SETUP_TOKEN` in `.env`. +- An instance brought up by hand without that variable has a token the API generated. The API writes it to its log once, on the first start. -The first registration receives the wildcard admin ACL, which is full access to the admin dashboard, unless registration is set to hold new accounts for approval. On a username instance that is the first registration with a password. On an email instance it is the first registration that includes an email address. Finishing the wizard grants that same ACL to whichever account completes it, when that account holds none. +```bash +docker compose logs api | grep -i 'setup token' +``` + +When the token is lost, set [`FLUXER_SETUP_TOKEN`](/operator/configuration/#setup-token) in `.env` and run `docker compose up -d`. + +The mobile app has no field for the token, so do the first-run setup in a browser. When the browser is already signed in to an account that is not an admin, the wizard says so and offers to sign out. + +The wizard then asks how members sign in. Usernames is the default and needs no email setup at all. Email suits an instance that sends mail through SMTP. The choice is fixed once the owner account exists. [Sign-in methods](/operator/sign-in/) explains both. + +Then create the owner account, and choose branding, registration mode, community policy, media expiry, integrations and premium settings. + +The registration that presents the setup token receives the wildcard admin ACL, which is full access to the admin dashboard, in every registration mode. On a username instance that registration needs a password. On an email instance it needs an email address. The token stops working once the admin account exists. On a username instance the wizard shows the owner's [recovery kit](/operator/sign-in/#recovery-kits) next. Store it somewhere safe. Without the kit, a forgotten owner password needs another admin to issue a [reset link](/operator/sign-in/#admin-reset-links). Register a passkey or a second admin account before you open registration. diff --git a/fluxer_docs/src/content/docs/operator/kubernetes.mdx b/fluxer_docs/src/content/docs/operator/kubernetes.mdx index 2825fd9f6..2b76a8159 100644 --- a/fluxer_docs/src/content/docs/operator/kubernetes.mdx +++ b/fluxer_docs/src/content/docs/operator/kubernetes.mdx @@ -63,9 +63,11 @@ helm install fluxer ./fluxer/deploy/helm/fluxer \ The first install pulls every image, creates the volumes and the buckets, and takes a few minutes. `helm test fluxer -n fluxer` then checks that the edge answers and serves the discovery document for the hostname. Keep `fluxer-values.yaml`, because every later upgrade passes it again. -:::caution[Finish setup before anyone else reaches the hostname] -An unconfigured instance lets any account that registers read and change the instance configuration. Register the first account and finish the wizard in the same sitting that brings the release up, as [Create the first admin](/operator/get-started/#first-run) describes. -::: +The setup wizard asks for the setup token, as [Create the first admin](/operator/get-started/#first-run) describes. The chart keeps it in the Secret: + +```bash +kubectl -n fluxer get secret fluxer-env -o jsonpath='{.data.FLUXER_SETUP_TOKEN}' | base64 -d +``` ## Exposure @@ -159,6 +161,7 @@ The chart keeps every secret in one Secret named `fluxer-env`. Each key below ta | `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64` | Generated, 32 random bytes in base64 | | `FLUXER_ADMIN_SECRET_KEY_BASE` | Generated, 64 letters and digits | | `FLUXER_ADMIN_OAUTH_CLIENT_SECRET` | Generated, 64 letters and digits | +| `FLUXER_SETUP_TOKEN` | Generated, 64 letters and digits | | `FLUXER_LIVEKIT_API_KEY` | The fixed value `fluxer` | | `FLUXER_LIVEKIT_API_SECRET` | Generated, 64 letters and digits | | `FLUXER_VAPID_PUBLIC_KEY` | Generated P-256 pair | @@ -176,7 +179,7 @@ kubectl -n fluxer exec postgres-0 -- psql -U fluxer -c "ALTER ROLE fluxer PASSWO ### A Secret you manage -Set `secrets.existingSecret` to the name of a Secret you create, holding every key in the table above. The chart then renders no Secret and never changes yours. The install stops when the Secret does not exist, lacks a key, or holds a setting that disagrees with one the chart sets itself, and it names the Secret and the keys. After editing it, restart the release so the pods read the new values: +Set `secrets.existingSecret` to the name of a Secret you create, holding every key in the table above. `FLUXER_SETUP_TOKEN` is the one optional key. Without it the API makes a token on its first start and writes it to its log once. The chart then renders no Secret and never changes yours. The install stops when the Secret does not exist, lacks a key, or holds a setting that disagrees with one the chart sets itself, and it names the Secret and the keys. After editing it, restart the release so the pods read the new values: ```bash kubectl -n fluxer rollout restart deployment,statefulset -l app.kubernetes.io/instance=fluxer @@ -315,7 +318,7 @@ kubectl -n fluxer exec postgres-0 -- sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POS Keep `fluxer-env.yaml` as safe as the database. Without it a restored database has no matching Postgres password, and every signed session and token stops working. -Uploads live on the volume `data-seaweedfs-0`. Snapshot it with your storage provider's VolumeSnapshot class. Without one, stop the store and copy the volume out through a helper pod: +Uploads are on the volume `data-seaweedfs-0`. Snapshot it with your storage provider's VolumeSnapshot class. Without one, stop the store and copy the volume out through a helper pod: ```bash kubectl -n fluxer scale statefulset seaweedfs --replicas=0 diff --git a/fluxer_docs/src/content/docs/operator/sign-in.mdx b/fluxer_docs/src/content/docs/operator/sign-in.mdx index 2a34f0e89..04ea3e5b2 100644 --- a/fluxer_docs/src/content/docs/operator/sign-in.mdx +++ b/fluxer_docs/src/content/docs/operator/sign-in.mdx @@ -45,7 +45,7 @@ Members register and sign in with an email address. Email verification and passw ## Choosing the method -The setup wizard asks how members sign in, straight after the theme step and before the owner account is created. Usernames is the recommended choice. Picking Email there switches the instance before the owner registers. +The setup wizard asks how members sign in, after the [setup token](/operator/get-started/#first-run) and theme steps and before the owner account is created. Usernames is the recommended choice. Picking Email there switches the instance before the owner registers. When Email is picked, the same step asks how usernames are tagged. No tags is the recommended choice. diff --git a/fluxer_docs/src/content/docs/operator/upgrading.mdx b/fluxer_docs/src/content/docs/operator/upgrading.mdx index 6d57938f9..5a7d20274 100644 --- a/fluxer_docs/src/content/docs/operator/upgrading.mdx +++ b/fluxer_docs/src/content/docs/operator/upgrading.mdx @@ -79,6 +79,8 @@ printf '\nFLUXER_PROFILE_PSEUDONYM_SECRET=%s\n' "$(openssl rand -hex 32)" >> .en Do not paste `FLUXER_ERLANG_COOKIE=$(openssl rand -hex 32)` into `.env`. Compose does not execute shell commands in that file. +An upgrade does not write `FLUXER_SETUP_TOKEN`. An instance with no admin account yet asks for the setup token after the upgrade. [Create the first admin](/operator/get-started/#first-run) has where to find it. An instance whose admin access was granted outside setup also logs a generated setup token after the upgrade. + ## Keep a local compose change An upgrade replaces the stack files, including direct edits to `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml`, `external-object-store.compose.yml` or the `Caddyfile`. Use `--dry-run` to see which files will change. diff --git a/fluxer_docs/src/content/docs/topics/announcement-channels.md b/fluxer_docs/src/content/docs/topics/announcement-channels.md index aaafdf096..f3b538566 100644 --- a/fluxer_docs/src/content/docs/topics/announcement-channels.md +++ b/fluxer_docs/src/content/docs/topics/announcement-channels.md @@ -4,7 +4,7 @@ title: Announcement channels description: Publishing messages from an announcement channel into the channels that follow it. --- -An announcement channel is a guild text channel, [type](/http-api/channels/#channel-types) `5`, whose messages can be published. Other channels follow it, and each published message is copied into every following channel. The copy is posted by a channel follower [webhook](/http-api/webhooks/#webhook-types) that lives in the following channel. +An announcement channel is a guild text channel, [type](/http-api/channels/#channel-types) `5`, whose messages can be published. Other channels follow it, and each published message is copied into every following channel. The copy is posted by a channel follower [webhook](/http-api/webhooks/#webhook-types) that is in the following channel. Every guild can create announcement channels. [Create guild channel](/http-api/guild-channels/#create-guild-channel) accepts `type` `5`, and an announcement channel has the fields, permissions, and slowmode of a text channel. diff --git a/fluxer_docs/src/installer/install.ps1 b/fluxer_docs/src/installer/install.ps1 index 781ffc6f7..08b87e949 100644 --- a/fluxer_docs/src/installer/install.ps1 +++ b/fluxer_docs/src/installer/install.ps1 @@ -196,6 +196,7 @@ $FluxerSecretKeys = @( @{Name = 'FLUXER_ADMIN_SECRET_KEY_BASE'; Kind = 'hex'} @{Name = 'FLUXER_ADMIN_OAUTH_CLIENT_SECRET'; Kind = 'hex'} @{Name = 'LIVEKIT_API_SECRET'; Kind = 'hex'} + @{Name = 'FLUXER_SETUP_TOKEN'; Kind = 'hex'} @{Name = 'FLUXER_VAPID_PUBLIC_KEY'; Kind = 'vapid_public'} @{Name = 'FLUXER_VAPID_PRIVATE_KEY'; Kind = 'vapid_private'} ) @@ -214,6 +215,13 @@ function Write-FluxerLine([string]$Message) { Write-Host $Message } +function Write-FluxerFirstRun([string]$EnvPath) { + Write-FluxerLine 'Open it and enter this setup token to create the admin account:' + Write-FluxerLine '' + Write-FluxerLine " $(Get-FluxerEnvValue $EnvPath 'FLUXER_SETUP_TOKEN')" + Write-FluxerLine '' +} + function Write-FluxerProblem([string]$Message) { [Console]::Error.WriteLine($Message) } @@ -692,7 +700,8 @@ function Move-FluxerStackFiles([string]$StagingDir, [string]$TargetDir) { # knows. FLUXER_VAPID_EMAIL is optional, and compose derives admin@FLUXER_DOMAIN while it is unset. # The five other non-secret keys in the list above ship correct in .env.example and need no edit. # -# Every secret in .env.example contains the literal CHANGE_ME. A key whose name ends in _BASE64 +# Every secret in .env.example contains the literal CHANGE_ME, apart from FLUXER_SETUP_TOKEN, +# which sits there as a commented line. A key whose name ends in _BASE64 # takes 32 random bytes as base64, every other key takes 32 random bytes as hex, and the VAPID pair # comes from the generator above. # @@ -1457,7 +1466,7 @@ function Get-FluxerFreeKb([string]$Path) { # docker compose up -d function Copy-FluxerVolumes([string]$Record, [string]$Project, [string]$TargetDir) { if (-not (Test-FluxerStackDefinesService 'seaweedfs' $TargetDir)) { - Write-FluxerLine 'Skipping the uploads copy. This stack defines no seaweedfs service, so its objects live outside the stack and only the operator of that store can copy them.' + Write-FluxerLine 'Skipping the uploads copy. This stack defines no seaweedfs service, so its objects are outside the stack and only the operator of that store can copy them.' return } $present = @() @@ -2263,7 +2272,8 @@ function Invoke-FluxerInstall { if ($NoStart) { Write-FluxerLine "Run docker compose up -d in $targetDir to start the instance." - Write-FluxerLine "Secrets live in $envPath. Back that file up." + Write-FluxerFirstRun $envPath + Write-FluxerLine "Secrets are in $envPath. Back that file up." exit 0 } @@ -2281,8 +2291,8 @@ function Invoke-FluxerInstall { } Test-FluxerPublicHealth $readyOrigin Write-FluxerLine "Instance ready at $readyOrigin" - Write-FluxerLine 'Open it and create the first admin account. Finish the setup wizard in the same sitting.' - Write-FluxerLine "Secrets live in $envPath. Back that file up." + Write-FluxerFirstRun $envPath + Write-FluxerLine "Secrets are in $envPath. Back that file up." } finally { Pop-Location } diff --git a/fluxer_docs/src/installer/install.sh b/fluxer_docs/src/installer/install.sh index ebd1dbe3e..3d0dddcc5 100644 --- a/fluxer_docs/src/installer/install.sh +++ b/fluxer_docs/src/installer/install.sh @@ -119,6 +119,7 @@ FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 base64 FLUXER_ADMIN_SECRET_KEY_BASE hex FLUXER_ADMIN_OAUTH_CLIENT_SECRET hex LIVEKIT_API_SECRET hex +FLUXER_SETUP_TOKEN hex FLUXER_VAPID_PUBLIC_KEY vapid_public FLUXER_VAPID_PRIVATE_KEY vapid_private KEYS @@ -241,6 +242,13 @@ fluxer_say() { printf '%s\n' "$1" } +fluxer_say_first_run() { + fluxer_say 'Open it and enter this setup token to create the admin account:' + fluxer_say '' + fluxer_say " $(fluxer_env_scalar FLUXER_SETUP_TOKEN)" + fluxer_say '' +} + fluxer_fail() { printf '%s\n' "$2" >&2 exit "$1" @@ -984,7 +992,8 @@ fluxer_generate_vapid() { # other non-secret keys in the list above ship correct in .env.example and need no # edit. # -# Every secret in .env.example contains the literal CHANGE_ME. A key whose name +# Every secret in .env.example contains the literal CHANGE_ME, apart from +# FLUXER_SETUP_TOKEN, which sits there as a commented line. A key whose name # ends in _BASE64 takes openssl rand -base64 32, every other key takes # openssl rand -hex 32, and the VAPID pair comes from the generator above. # @@ -1689,7 +1698,7 @@ fluxer_free_kb() { # docker compose up -d fluxer_copy_volumes() { if ! fluxer_stack_defines_service seaweedfs; then - fluxer_say 'Skipping the uploads copy. This stack defines no seaweedfs service, so its objects live outside the stack and only the operator of that store can copy them.' + fluxer_say 'Skipping the uploads copy. This stack defines no seaweedfs service, so its objects are outside the stack and only the operator of that store can copy them.' return 0 fi fluxer_backup_volumes > "$fluxer_scratch/backup-volumes" @@ -2240,8 +2249,8 @@ fluxer_say 'That .env serves https on 443, which is the only layout this script if [ "$opt_no_start" -eq 1 ]; then fluxer_say "Start the instance with $fluxer_engine compose up -d in $opt_dir." - fluxer_say 'Open it and create the first admin account. Finish the setup wizard in the same sitting.' - fluxer_say "Secrets live in $opt_dir/.env. Back that file up." + fluxer_say_first_run + fluxer_say "Secrets are in $opt_dir/.env. Back that file up." exit 0 fi @@ -2265,5 +2274,5 @@ fi fluxer_probe "$fluxer_ready_origin" fluxer_say "Instance ready at $fluxer_ready_origin" -fluxer_say 'Open it and create the first admin account. Finish the setup wizard in the same sitting.' -fluxer_say "Secrets live in $opt_dir/.env. Back that file up." +fluxer_say_first_run +fluxer_say "Secrets are in $opt_dir/.env. Back that file up." diff --git a/packages/config/src/MasterConfig.ts b/packages/config/src/MasterConfig.ts index 1593fa4bf..ea241b5d5 100644 --- a/packages/config/src/MasterConfig.ts +++ b/packages/config/src/MasterConfig.ts @@ -301,6 +301,7 @@ export interface MasterConfig { branding: InstanceBrandingConfig; setup: { configured: boolean; + token?: string; }; account_identity: AccountIdentityModeName | null; tag_style: TagStyleName | null; diff --git a/packages/config/src/__tests__/EnvironmentOverrides.test.ts b/packages/config/src/__tests__/EnvironmentOverrides.test.ts index d527debe7..f51eb1bb9 100644 --- a/packages/config/src/__tests__/EnvironmentOverrides.test.ts +++ b/packages/config/src/__tests__/EnvironmentOverrides.test.ts @@ -50,6 +50,12 @@ describe('buildNamedFluxerEnvOverrides', () => { ).toEqual({}); }); + test('maps the setup token onto the instance setup section', () => { + expect(buildNamedFluxerEnvOverrides({FLUXER_SETUP_TOKEN: 'a-setup-token-of-at-least-32-characters'})).toEqual({ + instance: {setup: {token: 'a-setup-token-of-at-least-32-characters'}}, + }); + }); + test('a blank canonical name falls through to its alias', () => { expect( buildNamedFluxerEnvOverrides({ diff --git a/packages/config/src/config_loader/EnvironmentOverrides.ts b/packages/config/src/config_loader/EnvironmentOverrides.ts index a29802c0c..4a22e5eed 100644 --- a/packages/config/src/config_loader/EnvironmentOverrides.ts +++ b/packages/config/src/config_loader/EnvironmentOverrides.ts @@ -288,6 +288,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record = { FLUXER_APP_STATUS_PAGE_URL: {path: ['instance', 'branding', 'status_page_url']}, FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL: {path: ['instance', 'branding', 'status_page_incident_history_url']}, FLUXER_INSTANCE_SETUP_CONFIGURED: {path: ['instance', 'setup', 'configured'], parse: parseBoolean}, + FLUXER_SETUP_TOKEN: {path: ['instance', 'setup', 'token']}, FLUXER_ACCOUNT_IDENTITY: {path: ['instance', 'account_identity'], parse: parseAccountIdentity}, FLUXER_TAG_STYLE: {path: ['instance', 'tag_style'], parse: parseTagStyle}, FLUXER_DISCOVERY_ENABLED: {path: ['discovery', 'enabled'], parse: parseBoolean}, diff --git a/packages/constants/src/ValidationErrorCodes.ts b/packages/constants/src/ValidationErrorCodes.ts index 03ab35aaf..6f7c38425 100644 --- a/packages/constants/src/ValidationErrorCodes.ts +++ b/packages/constants/src/ValidationErrorCodes.ts @@ -181,6 +181,8 @@ export const ValidationErrorCodes = { RECIPIENT_IDS_MUST_BE_VALID_SNOWFLAKES: 'RECIPIENT_IDS_MUST_BE_VALID_SNOWFLAKES', REFERENCED_ATTACHMENT_NOT_FOUND: 'REFERENCED_ATTACHMENT_NOT_FOUND', SESSION_TIMEOUT: 'SESSION_TIMEOUT', + SETUP_TOKEN_INVALID: 'SETUP_TOKEN_INVALID', + SETUP_TOKEN_REQUIRED: 'SETUP_TOKEN_REQUIRED', SSO_IDENTITY_MISMATCH: 'SSO_IDENTITY_MISMATCH', SSO_MISCONFIGURED: 'SSO_MISCONFIGURED', SSO_PROVIDER_DID_NOT_RETURN_EMAIL: 'SSO_PROVIDER_DID_NOT_RETURN_EMAIL', diff --git a/packages/errors/src/i18n/ErrorCodeMappings.ts b/packages/errors/src/i18n/ErrorCodeMappings.ts index f3ee3443f..5e0ae92a4 100644 --- a/packages/errors/src/i18n/ErrorCodeMappings.ts +++ b/packages/errors/src/i18n/ErrorCodeMappings.ts @@ -461,6 +461,8 @@ export const ErrorCodeToI18nKey = { [ValidationErrorCodes.PASSWORD_IS_TOO_COMMON]: 'auth_and_registration.password_is_too_common', [ValidationErrorCodes.PASSWORD_NOT_SET]: 'auth_and_registration.password_not_set', [ValidationErrorCodes.SESSION_TIMEOUT]: 'account.session_timeout', + [ValidationErrorCodes.SETUP_TOKEN_INVALID]: 'auth_and_registration.setup_token_invalid', + [ValidationErrorCodes.SETUP_TOKEN_REQUIRED]: 'auth_and_registration.setup_token_required', [ValidationErrorCodes.SSO_IDENTITY_MISMATCH]: 'auth_and_oauth.sso_identity_mismatch', [ValidationErrorCodes.SSO_MISCONFIGURED]: 'auth_and_oauth.sso_misconfigured', [ValidationErrorCodes.SSO_PROVIDER_DID_NOT_RETURN_EMAIL]: 'auth_and_oauth.sso_provider_did_not_return_email', diff --git a/packages/errors/src/i18n/ErrorI18nMessages.ts b/packages/errors/src/i18n/ErrorI18nMessages.ts index 788422425..d595b39af 100644 --- a/packages/errors/src/i18n/ErrorI18nMessages.ts +++ b/packages/errors/src/i18n/ErrorI18nMessages.ts @@ -128,6 +128,9 @@ export const ERROR_I18N_MESSAGES = { 'auth_and_registration.password_is_too_common': 'Your password is too common. Please choose a less predictable password.', 'auth_and_registration.password_not_set': "Password isn't set.", + 'auth_and_registration.setup_token_invalid': 'That setup token is not correct. Check the token and try again.', + 'auth_and_registration.setup_token_required': + 'This instance is not set up yet. Finish setup in a web browser with the setup token before creating accounts.', 'auth_and_registration.username_sign_in_only': 'This is only available on instances where people sign in with a username.', 'bots.bot_discriminator_cannot_be_changed': "Bot discriminator can't be changed.", diff --git a/packages/errors/src/i18n/weblate/locales/ar.json b/packages/errors/src/i18n/weblate/locales/ar.json index e53aab080..d02a54450 100644 --- a/packages/errors/src/i18n/weblate/locales/ar.json +++ b/packages/errors/src/i18n/weblate/locales/ar.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "اسم المستخدم أو مفتاح الاسترداد غير صحيح.", "auth_and_registration.password_is_too_common": "كلمة مرورك شائعة جدًا. اختر كلمة مرور يصعب تخمينها.", "auth_and_registration.password_not_set": "لم تُعيّن كلمة المرور.", + "auth_and_registration.setup_token_invalid": "رمز الإعداد هذا غير صحيح. تحقق من الرمز وحاول مرة أخرى.", + "auth_and_registration.setup_token_required": "لم يتم إعداد هذا المثيل بعد. أكمل الإعداد في متصفح ويب باستخدام رمز الإعداد قبل إنشاء الحسابات.", "auth_and_registration.username_sign_in_only": "هذا متاح فقط على المثيلات التي يسجّل فيها الأشخاص الدخول باسم مستخدم.", "bots.bot_discriminator_cannot_be_changed": "لا يمكن تغيير مميّز البوت.", "bots.bot_is_private": "هذا البوت خاص.", diff --git a/packages/errors/src/i18n/weblate/locales/bg.json b/packages/errors/src/i18n/weblate/locales/bg.json index 5d449c9d4..8fca6e8ab 100644 --- a/packages/errors/src/i18n/weblate/locales/bg.json +++ b/packages/errors/src/i18n/weblate/locales/bg.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Грешно потребителско име или ключ за възстановяване.", "auth_and_registration.password_is_too_common": "Паролата ти е твърде често срещана. Избери по-трудна за отгатване парола.", "auth_and_registration.password_not_set": "Паролата не е зададена.", + "auth_and_registration.setup_token_invalid": "Този токен за настройка не е правилен. Проверете токена и опитайте отново.", + "auth_and_registration.setup_token_required": "Тази инстанция все още не е настроена. Завършете настройката в уеб браузър с токена за настройка, преди да създавате акаунти.", "auth_and_registration.username_sign_in_only": "Това е налично само на инстанции, където хората се вписват с потребителско име.", "bots.bot_discriminator_cannot_be_changed": "Дискриминаторът на бота не може да се променя.", "bots.bot_is_private": "Този бот е частен.", diff --git a/packages/errors/src/i18n/weblate/locales/cs.json b/packages/errors/src/i18n/weblate/locales/cs.json index 45a7433bd..d4ffc525f 100644 --- a/packages/errors/src/i18n/weblate/locales/cs.json +++ b/packages/errors/src/i18n/weblate/locales/cs.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Neplatné uživatelské jméno nebo klíč pro obnovení.", "auth_and_registration.password_is_too_common": "Vaše heslo je příliš běžné. Zvolte si hůře odhadnutelné heslo.", "auth_and_registration.password_not_set": "Heslo není nastavené.", + "auth_and_registration.setup_token_invalid": "Tento nastavovací token není správný. Zkontrolujte token a zkuste to znovu.", + "auth_and_registration.setup_token_required": "Tato instance ještě není nastavená. Dokončete nastavení v prohlížeči pomocí nastavovacího tokenu před vytvořením účtů.", "auth_and_registration.username_sign_in_only": "Tato možnost je dostupná jen na instancích, kde se lidé přihlašují uživatelským jménem.", "bots.bot_discriminator_cannot_be_changed": "Čtyřmístný identifikátor bota nelze změnit.", "bots.bot_is_private": "Tento bot je soukromý.", diff --git a/packages/errors/src/i18n/weblate/locales/da.json b/packages/errors/src/i18n/weblate/locales/da.json index 488fd762f..237eae833 100644 --- a/packages/errors/src/i18n/weblate/locales/da.json +++ b/packages/errors/src/i18n/weblate/locales/da.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Ugyldigt brugernavn eller gendannelsesnøgle.", "auth_and_registration.password_is_too_common": "Din adgangskode er for almindelig. Vælg en adgangskode, der er sværere at gætte.", "auth_and_registration.password_not_set": "Der er ikke angivet nogen adgangskode.", + "auth_and_registration.setup_token_invalid": "Det opsætningstoken er ikke korrekt. Tjek tokenet, og prøv igen.", + "auth_and_registration.setup_token_required": "Denne instans er endnu ikke sat op. Afslut opsætningen i en webbrowser med opsætningstokenet, før du opretter konti.", "auth_and_registration.username_sign_in_only": "Dette er kun tilgængeligt på instanser, hvor man logger ind med et brugernavn.", "bots.bot_discriminator_cannot_be_changed": "En bots tag kan ikke ændres.", "bots.bot_is_private": "Denne bot er privat.", diff --git a/packages/errors/src/i18n/weblate/locales/de.json b/packages/errors/src/i18n/weblate/locales/de.json index af0593894..ca9ec4b75 100644 --- a/packages/errors/src/i18n/weblate/locales/de.json +++ b/packages/errors/src/i18n/weblate/locales/de.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Der Benutzername oder der Wiederherstellungsschlüssel ist ungültig.", "auth_and_registration.password_is_too_common": "Dein Passwort wird zu häufig verwendet. Wähle bitte ein Passwort, das weniger leicht zu erraten ist.", "auth_and_registration.password_not_set": "Das Passwort ist nicht festgelegt.", + "auth_and_registration.setup_token_invalid": "Dieses Einrichtungstoken ist nicht korrekt. Überprüfe das Token und versuche es erneut.", + "auth_and_registration.setup_token_required": "Diese Instanz ist noch nicht eingerichtet. Schließe die Einrichtung in einem Webbrowser mit dem Einrichtungstoken ab, bevor du Konten erstellst.", "auth_and_registration.username_sign_in_only": "Das ist nur auf Instanzen verfügbar, auf denen man sich mit einem Benutzernamen anmeldet.", "bots.bot_discriminator_cannot_be_changed": "Der Bot-Discriminator kann nicht geändert werden.", "bots.bot_is_private": "Dieser Bot ist privat.", diff --git a/packages/errors/src/i18n/weblate/locales/el.json b/packages/errors/src/i18n/weblate/locales/el.json index ef172b68f..625ac82c0 100644 --- a/packages/errors/src/i18n/weblate/locales/el.json +++ b/packages/errors/src/i18n/weblate/locales/el.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Λανθασμένο όνομα χρήστη ή κλειδί ανάκτησης.", "auth_and_registration.password_is_too_common": "Ο κωδικός πρόσβασής σου είναι πολύ κοινός. Διάλεξε έναν λιγότερο προβλέψιμο κωδικό πρόσβασης.", "auth_and_registration.password_not_set": "Ο κωδικός πρόσβασης δεν έχει οριστεί.", + "auth_and_registration.setup_token_invalid": "Αυτό το διακριτικό ρύθμισης δεν είναι σωστό. Ελέγξτε το διακριτικό και δοκιμάστε ξανά.", + "auth_and_registration.setup_token_required": "Αυτή η παρουσία δεν έχει ρυθμιστεί ακόμα. Ολοκληρώστε τη ρύθμιση σε ένα πρόγραμμα περιήγησης ιστού με το διακριτικό ρύθμισης πριν δημιουργήσετε λογαριασμούς.", "auth_and_registration.username_sign_in_only": "Αυτό είναι διαθέσιμο μόνο σε εγκαταστάσεις όπου η σύνδεση γίνεται με όνομα χρήστη.", "bots.bot_discriminator_cannot_be_changed": "Το διακριτικό του bot δεν μπορεί να αλλάξει.", "bots.bot_is_private": "Αυτό το bot είναι ιδιωτικό.", diff --git a/packages/errors/src/i18n/weblate/locales/en-GB.json b/packages/errors/src/i18n/weblate/locales/en-GB.json index ac88c52ba..e42b652ac 100644 --- a/packages/errors/src/i18n/weblate/locales/en-GB.json +++ b/packages/errors/src/i18n/weblate/locales/en-GB.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Invalid username or recovery key.", "auth_and_registration.password_is_too_common": "Your password is too common. Please choose a less predictable password.", "auth_and_registration.password_not_set": "Password isn't set.", + "auth_and_registration.setup_token_invalid": "That setup token is not correct. Check the token and try again.", + "auth_and_registration.setup_token_required": "This instance is not set up yet. Finish setup in a web browser with the setup token before creating accounts.", "auth_and_registration.username_sign_in_only": "This is only available on instances where people sign in with a username.", "bots.bot_discriminator_cannot_be_changed": "Bot discriminator can't be changed.", "bots.bot_is_private": "This bot is private.", diff --git a/packages/errors/src/i18n/weblate/locales/es-419.json b/packages/errors/src/i18n/weblate/locales/es-419.json index af31c78b3..c9bc13719 100644 --- a/packages/errors/src/i18n/weblate/locales/es-419.json +++ b/packages/errors/src/i18n/weblate/locales/es-419.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Nombre de usuario o clave de recuperación no válidos.", "auth_and_registration.password_is_too_common": "Tu contraseña es demasiado común. Elige una contraseña menos predecible.", "auth_and_registration.password_not_set": "La contraseña no está configurada.", + "auth_and_registration.setup_token_invalid": "Ese token de configuración no es correcto. Verifica el token e inténtalo de nuevo.", + "auth_and_registration.setup_token_required": "Esta instancia aún no está configurada. Finaliza la configuración en un navegador web con el token de configuración antes de crear cuentas.", "auth_and_registration.username_sign_in_only": "Esto solo está disponible en instancias donde se inicia sesión con un nombre de usuario.", "bots.bot_discriminator_cannot_be_changed": "El discriminador del bot no se puede cambiar.", "bots.bot_is_private": "Este bot es privado.", diff --git a/packages/errors/src/i18n/weblate/locales/es-ES.json b/packages/errors/src/i18n/weblate/locales/es-ES.json index cbfde8a3d..585c0e0ba 100644 --- a/packages/errors/src/i18n/weblate/locales/es-ES.json +++ b/packages/errors/src/i18n/weblate/locales/es-ES.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Nombre de usuario o clave de recuperación no válidos.", "auth_and_registration.password_is_too_common": "Tu contraseña es demasiado común. Elige una contraseña menos predecible.", "auth_and_registration.password_not_set": "La contraseña no está establecida.", + "auth_and_registration.setup_token_invalid": "Ese token de configuración no es correcto. Comprueba el token e inténtalo de nuevo.", + "auth_and_registration.setup_token_required": "Esta instancia aún no está configurada. Completa la configuración en un navegador web con el token de configuración antes de crear cuentas.", "auth_and_registration.username_sign_in_only": "Esto solo está disponible en instancias donde se inicia sesión con un nombre de usuario.", "bots.bot_discriminator_cannot_be_changed": "El discriminador del bot no se puede cambiar.", "bots.bot_is_private": "Este bot es privado.", diff --git a/packages/errors/src/i18n/weblate/locales/fi.json b/packages/errors/src/i18n/weblate/locales/fi.json index 50b556dfe..9e860d97a 100644 --- a/packages/errors/src/i18n/weblate/locales/fi.json +++ b/packages/errors/src/i18n/weblate/locales/fi.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Virheellinen käyttäjänimi tai palautusavain.", "auth_and_registration.password_is_too_common": "Salasanasi on liian yleinen. Valitse vaikeammin arvattava salasana.", "auth_and_registration.password_not_set": "Salasanaa ei ole asetettu.", + "auth_and_registration.setup_token_invalid": "Tuo asennustunnus ei ole oikein. Tarkista tunnus ja yritä uudelleen.", + "auth_and_registration.setup_token_required": "Tätä instanssia ei ole vielä määritetty. Viimeistele määritys verkkoselaimessa asennustunnuksella ennen tilien luomista.", "auth_and_registration.username_sign_in_only": "Tämä on käytettävissä vain instansseissa, joissa kirjaudutaan käyttäjänimellä.", "bots.bot_discriminator_cannot_be_changed": "Botin nelinumeroista tunnistetta ei voi muuttaa.", "bots.bot_is_private": "Tämä botti on yksityinen.", diff --git a/packages/errors/src/i18n/weblate/locales/fr.json b/packages/errors/src/i18n/weblate/locales/fr.json index 52e3bbd05..6cc52afec 100644 --- a/packages/errors/src/i18n/weblate/locales/fr.json +++ b/packages/errors/src/i18n/weblate/locales/fr.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Nom d'utilisateur ou clé de récupération invalide.", "auth_and_registration.password_is_too_common": "Votre mot de passe est trop courant. Choisissez-en un moins prévisible.", "auth_and_registration.password_not_set": "Le mot de passe n'est pas défini.", + "auth_and_registration.setup_token_invalid": "Ce jeton d'installation n'est pas correct. Vérifiez le jeton et réessayez.", + "auth_and_registration.setup_token_required": "Cette instance n'est pas encore configurée. Terminez la configuration dans un navigateur web avec le jeton d'installation avant de créer des comptes.", "auth_and_registration.username_sign_in_only": "Ceci n'est disponible que sur les instances où l'on se connecte avec un nom d'utilisateur.", "bots.bot_discriminator_cannot_be_changed": "Le discriminateur du bot ne peut pas être modifié.", "bots.bot_is_private": "Ce bot est privé.", diff --git a/packages/errors/src/i18n/weblate/locales/he.json b/packages/errors/src/i18n/weblate/locales/he.json index 99c5626f0..6dd826d35 100644 --- a/packages/errors/src/i18n/weblate/locales/he.json +++ b/packages/errors/src/i18n/weblate/locales/he.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "שם משתמש או מפתח שחזור שגויים.", "auth_and_registration.password_is_too_common": "הסיסמה שלך נפוצה מדי. מומלץ לבחור סיסמה פחות צפויה.", "auth_and_registration.password_not_set": "סיסמה לא הוגדרה.", + "auth_and_registration.setup_token_invalid": "אסימון ההגדרה הזה אינו תקין. בדוק את האסימון ונסה שוב.", + "auth_and_registration.setup_token_required": "מופע זה עדיין לא הוגדר. השלם את ההגדרה בדפדפן אינטרנט באמצעות אסימון ההגדרה לפני יצירת חשבונות.", "auth_and_registration.username_sign_in_only": "אפשרות זו זמינה רק במופעים שבהם מתחברים עם שם משתמש.", "bots.bot_discriminator_cannot_be_changed": "לא ניתן לשנות את המזהה של הבוט.", "bots.bot_is_private": "הבוט הזה פרטי.", diff --git a/packages/errors/src/i18n/weblate/locales/hi.json b/packages/errors/src/i18n/weblate/locales/hi.json index 252052541..ac2c61574 100644 --- a/packages/errors/src/i18n/weblate/locales/hi.json +++ b/packages/errors/src/i18n/weblate/locales/hi.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "अमान्य यूज़रनेम या रिकवरी की।", "auth_and_registration.password_is_too_common": "आपका पासवर्ड बहुत कॉमन है। कृपया ऐसा पासवर्ड चुनें जिसका अंदाज़ा लगाना मुश्किल हो।", "auth_and_registration.password_not_set": "पासवर्ड सेट नहीं है।", + "auth_and_registration.setup_token_invalid": "वह सेटअप टोकन सही नहीं है। टोकन जांचें और पुनः प्रयास करें।", + "auth_and_registration.setup_token_required": "यह इंस्टेंस अभी तक सेट अप नहीं हुआ है। खाते बनाने से पहले सेटअप टोकन के साथ वेब ब्राउज़र में सेटअप पूरा करें।", "auth_and_registration.username_sign_in_only": "यह सिर्फ़ उन इंस्टेंस पर उपलब्ध है जहाँ लोग यूज़रनेम से साइन इन करते हैं।", "bots.bot_discriminator_cannot_be_changed": "बॉट डिस्क्रिमिनेटर को बदला नहीं जा सकता।", "bots.bot_is_private": "यह बॉट प्राइवेट है।", diff --git a/packages/errors/src/i18n/weblate/locales/hr.json b/packages/errors/src/i18n/weblate/locales/hr.json index 4ec510a26..7f79f12f8 100644 --- a/packages/errors/src/i18n/weblate/locales/hr.json +++ b/packages/errors/src/i18n/weblate/locales/hr.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Neispravno korisničko ime ili ključ za oporavak.", "auth_and_registration.password_is_too_common": "Tvoja je lozinka previše uobičajena. Odaberi manje predvidljivu lozinku.", "auth_and_registration.password_not_set": "Lozinka nije postavljena.", + "auth_and_registration.setup_token_invalid": "Taj token za postavljanje nije ispravan. Provjerite token i pokušajte ponovno.", + "auth_and_registration.setup_token_required": "Ova instanca još nije postavljena. Završite postavljanje u web-pregledniku pomoću tokena za postavljanje prije stvaranja računa.", "auth_and_registration.username_sign_in_only": "Ovo je dostupno samo na instancama na kojima se prijavljuje korisničkim imenom.", "bots.bot_discriminator_cannot_be_changed": "Diskriminator bota ne može se promijeniti.", "bots.bot_is_private": "Ovaj bot je privatan.", diff --git a/packages/errors/src/i18n/weblate/locales/hu.json b/packages/errors/src/i18n/weblate/locales/hu.json index f9fbf59d5..99723c6c9 100644 --- a/packages/errors/src/i18n/weblate/locales/hu.json +++ b/packages/errors/src/i18n/weblate/locales/hu.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Érvénytelen felhasználónév vagy helyreállítási kulcs.", "auth_and_registration.password_is_too_common": "A jelszavad túl gyakori. Válassz nehezebben kitalálható jelszót.", "auth_and_registration.password_not_set": "Nincs beállítva jelszó.", + "auth_and_registration.setup_token_invalid": "A megadott beállítási token nem helyes. Ellenőrizd a tokent, és próbáld meg újra.", + "auth_and_registration.setup_token_required": "Ez a példány még nincs beállítva. Fejezd be a beállítást egy webböngészőben a beállítási token használatával, mielőtt fiókokat hoznál létre.", "auth_and_registration.username_sign_in_only": "Ez csak olyan példányokon érhető el, ahol felhasználónévvel lehet bejelentkezni.", "bots.bot_discriminator_cannot_be_changed": "A bot számazonosítója nem módosítható.", "bots.bot_is_private": "Ez a bot zárt.", diff --git a/packages/errors/src/i18n/weblate/locales/id.json b/packages/errors/src/i18n/weblate/locales/id.json index 421449dfb..4e8bca306 100644 --- a/packages/errors/src/i18n/weblate/locales/id.json +++ b/packages/errors/src/i18n/weblate/locales/id.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Nama pengguna atau kunci pemulihan tidak valid.", "auth_and_registration.password_is_too_common": "Kata sandimu terlalu umum. Pilih kata sandi yang lebih sulit ditebak.", "auth_and_registration.password_not_set": "Kata sandi belum diatur.", + "auth_and_registration.setup_token_invalid": "Token penyiapan itu tidak benar. Periksa token dan coba lagi.", + "auth_and_registration.setup_token_required": "Instans ini belum diatur. Selesaikan penyiapan di peramban web dengan token penyiapan sebelum membuat akun.", "auth_and_registration.username_sign_in_only": "Ini hanya tersedia di instance yang masuk dengan nama pengguna.", "bots.bot_discriminator_cannot_be_changed": "Diskriminator bot tidak dapat diubah.", "bots.bot_is_private": "Bot ini bersifat pribadi.", diff --git a/packages/errors/src/i18n/weblate/locales/it.json b/packages/errors/src/i18n/weblate/locales/it.json index c842f987a..ff6b62841 100644 --- a/packages/errors/src/i18n/weblate/locales/it.json +++ b/packages/errors/src/i18n/weblate/locales/it.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Nome utente o chiave di recupero non validi.", "auth_and_registration.password_is_too_common": "La tua password è troppo comune. Scegli una password meno prevedibile.", "auth_and_registration.password_not_set": "La password non è impostata.", + "auth_and_registration.setup_token_invalid": "Il token di configurazione non è corretto. Controlla il token e riprova.", + "auth_and_registration.setup_token_required": "Questa istanza non è ancora stata configurata. Completa la configurazione in un browser web con il token di configurazione prima di creare account.", "auth_and_registration.username_sign_in_only": "Disponibile solo sulle istanze in cui si accede con un nome utente.", "bots.bot_discriminator_cannot_be_changed": "Il discriminatore del bot non può essere modificato.", "bots.bot_is_private": "Questo bot è privato.", diff --git a/packages/errors/src/i18n/weblate/locales/ja.json b/packages/errors/src/i18n/weblate/locales/ja.json index 99a54803c..3e851782c 100644 --- a/packages/errors/src/i18n/weblate/locales/ja.json +++ b/packages/errors/src/i18n/weblate/locales/ja.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "ユーザー名またはリカバリーキーが正しくありません。", "auth_and_registration.password_is_too_common": "パスワードが一般的すぎます。より推測されにくいパスワードを設定してください。", "auth_and_registration.password_not_set": "パスワードが未設定です。", + "auth_and_registration.setup_token_invalid": "そのセットアップトークンは正しくありません。トークンを確認して、もう一度お試しください。", + "auth_and_registration.setup_token_required": "このインスタンスはまだセットアップされていません。アカウントを作成する前に、セットアップトークンを使用してウェブブラウザでセットアップを完了してください。", "auth_and_registration.username_sign_in_only": "この機能は、ユーザー名でサインインするインスタンスでのみ利用できます。", "bots.bot_discriminator_cannot_be_changed": "ボットの識別子は変更できません。", "bots.bot_is_private": "このボットは非公開です。", diff --git a/packages/errors/src/i18n/weblate/locales/ko.json b/packages/errors/src/i18n/weblate/locales/ko.json index dfe3c99c3..05ad055d7 100644 --- a/packages/errors/src/i18n/weblate/locales/ko.json +++ b/packages/errors/src/i18n/weblate/locales/ko.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "사용자 이름 또는 복구 키가 올바르지 않아요.", "auth_and_registration.password_is_too_common": "비밀번호가 너무 흔해요. 추측하기 어려운 비밀번호를 사용해 주세요.", "auth_and_registration.password_not_set": "비밀번호가 설정되지 않았어요.", + "auth_and_registration.setup_token_invalid": "해당 설정 토큰이 올바르지 않습니다. 토큰을 확인하고 다시 시도해 주세요.", + "auth_and_registration.setup_token_required": "이 인스턴스는 아직 설정되지 않았습니다. 계정을 만들기 전에 설정 토큰을 사용하여 웹 브라우저에서 설정을 완료하세요.", "auth_and_registration.username_sign_in_only": "사용자 이름으로 로그인하는 인스턴스에서만 사용할 수 있어요.", "bots.bot_discriminator_cannot_be_changed": "봇 디스크리미네이터를 변경할 수 없어요.", "bots.bot_is_private": "이 봇은 비공개예요.", diff --git a/packages/errors/src/i18n/weblate/locales/lt.json b/packages/errors/src/i18n/weblate/locales/lt.json index 57ebd6ad7..28fbbebd0 100644 --- a/packages/errors/src/i18n/weblate/locales/lt.json +++ b/packages/errors/src/i18n/weblate/locales/lt.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Netinkamas naudotojo vardas arba atkūrimo raktas.", "auth_and_registration.password_is_too_common": "Tavo slaptažodis per dažnai naudojamas. Pasirink sunkiau nuspėjamą slaptažodį.", "auth_and_registration.password_not_set": "Slaptažodis nenustatytas.", + "auth_and_registration.setup_token_invalid": "Šis sąrankos žetonas neteisingas. Patikrinkite žetoną ir bandykite dar kartą.", + "auth_and_registration.setup_token_required": "Ši instancija dar nėra nustatyta. Baigkite sąranką žiniatinklio naršyklėje naudodami sąrankos žetoną prieš kurdami paskyras.", "auth_and_registration.username_sign_in_only": "Tai galima tik instancijose, kuriose jungiamasi naudotojo vardu.", "bots.bot_discriminator_cannot_be_changed": "Boto diskriminatoriaus negalima pakeisti.", "bots.bot_is_private": "Šis botas yra privatus.", diff --git a/packages/errors/src/i18n/weblate/locales/nl.json b/packages/errors/src/i18n/weblate/locales/nl.json index f6073e6a0..1f3a5ebe2 100644 --- a/packages/errors/src/i18n/weblate/locales/nl.json +++ b/packages/errors/src/i18n/weblate/locales/nl.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Ongeldige gebruikersnaam of herstelsleutel.", "auth_and_registration.password_is_too_common": "Je wachtwoord komt te vaak voor. Kies een minder voorspelbaar wachtwoord.", "auth_and_registration.password_not_set": "Wachtwoord is niet ingesteld.", + "auth_and_registration.setup_token_invalid": "Die instellingstoken is niet correct. Controleer de token en probeer het opnieuw.", + "auth_and_registration.setup_token_required": "Deze instantie is nog niet ingesteld. Voltooi de installatie in een webbrowser met de instellingstoken voordat je accounts aanmaakt.", "auth_and_registration.username_sign_in_only": "Dit is alleen beschikbaar op instanties waar je inlogt met een gebruikersnaam.", "bots.bot_discriminator_cannot_be_changed": "De discriminator van een bot kan niet worden gewijzigd.", "bots.bot_is_private": "Deze bot is privé.", diff --git a/packages/errors/src/i18n/weblate/locales/no.json b/packages/errors/src/i18n/weblate/locales/no.json index cde7311b2..0ed350517 100644 --- a/packages/errors/src/i18n/weblate/locales/no.json +++ b/packages/errors/src/i18n/weblate/locales/no.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Ugyldig brukernavn eller gjenopprettingsnøkkel.", "auth_and_registration.password_is_too_common": "Passordet ditt er for vanlig. Velg et passord som er vanskeligere å gjette.", "auth_and_registration.password_not_set": "Det er ikke angitt noe passord.", + "auth_and_registration.setup_token_invalid": "Oppsettstokenen er ikke riktig. Sjekk tokenen og prøv igjen.", + "auth_and_registration.setup_token_required": "Denne instansen er ikke satt opp ennå. Fullfør oppsettet i en nettleser med oppsettstokenen før du oppretter kontoer.", "auth_and_registration.username_sign_in_only": "Dette er bare tilgjengelig på instanser der man logger inn med brukernavn.", "bots.bot_discriminator_cannot_be_changed": "Botens diskriminator kan ikke endres.", "bots.bot_is_private": "Denne boten er privat.", diff --git a/packages/errors/src/i18n/weblate/locales/pl.json b/packages/errors/src/i18n/weblate/locales/pl.json index 9fde1ecdd..89e4b359d 100644 --- a/packages/errors/src/i18n/weblate/locales/pl.json +++ b/packages/errors/src/i18n/weblate/locales/pl.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Niepoprawna nazwa użytkownika lub klucz odzyskiwania.", "auth_and_registration.password_is_too_common": "Twoje hasło jest zbyt popularne. Wybierz mniej przewidywalne hasło.", "auth_and_registration.password_not_set": "Hasło nie jest ustawione.", + "auth_and_registration.setup_token_invalid": "Ten token konfiguracji jest nieprawidłowy. Sprawdź token i spróbuj ponownie.", + "auth_and_registration.setup_token_required": "Ta instancja nie została jeszcze skonfigurowana. Zakończ konfigurację w przeglądarce internetowej za pomocą tokenu konfiguracji przed utworzeniem kont.", "auth_and_registration.username_sign_in_only": "Ta opcja jest dostępna tylko na instancjach, na których loguje się nazwą użytkownika.", "bots.bot_discriminator_cannot_be_changed": "Nie można zmienić dyskryminatora bota.", "bots.bot_is_private": "Ten bot jest prywatny.", diff --git a/packages/errors/src/i18n/weblate/locales/pt-BR.json b/packages/errors/src/i18n/weblate/locales/pt-BR.json index e116014b1..52bcb13db 100644 --- a/packages/errors/src/i18n/weblate/locales/pt-BR.json +++ b/packages/errors/src/i18n/weblate/locales/pt-BR.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Nome de usuário ou chave de recuperação inválidos.", "auth_and_registration.password_is_too_common": "Sua senha é muito comum. Escolha uma senha menos previsível.", "auth_and_registration.password_not_set": "A senha não foi definida.", + "auth_and_registration.setup_token_invalid": "Esse token de configuração não está correto. Verifique o token e tente novamente.", + "auth_and_registration.setup_token_required": "Esta instância ainda não foi configurada. Conclua a configuração em um navegador da web com o token de configuração antes de criar contas.", "auth_and_registration.username_sign_in_only": "Isso só está disponível em instâncias onde o login é feito com nome de usuário.", "bots.bot_discriminator_cannot_be_changed": "O discriminador do bot não pode ser alterado.", "bots.bot_is_private": "Este bot é privado.", diff --git a/packages/errors/src/i18n/weblate/locales/ro.json b/packages/errors/src/i18n/weblate/locales/ro.json index c56e2c0c2..d976f56eb 100644 --- a/packages/errors/src/i18n/weblate/locales/ro.json +++ b/packages/errors/src/i18n/weblate/locales/ro.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Nume de utilizator sau cheie de recuperare incorecte.", "auth_and_registration.password_is_too_common": "Parola ta este prea comună. Alege o parolă mai greu de ghicit.", "auth_and_registration.password_not_set": "Parola nu este setată.", + "auth_and_registration.setup_token_invalid": "Tokenul de configurare nu este corect. Verifică tokenul și încearcă din nou.", + "auth_and_registration.setup_token_required": "Instanța nu este configurată încă. Finalizează configurarea într-un browser web cu tokenul de configurare înainte de a crea conturi.", "auth_and_registration.username_sign_in_only": "Această opțiune este disponibilă doar pe instanțele unde autentificarea se face cu un nume de utilizator.", "bots.bot_discriminator_cannot_be_changed": "Discriminatorul botului nu poate fi schimbat.", "bots.bot_is_private": "Acest bot este privat.", diff --git a/packages/errors/src/i18n/weblate/locales/ru.json b/packages/errors/src/i18n/weblate/locales/ru.json index 03cbfd8d0..782e12008 100644 --- a/packages/errors/src/i18n/weblate/locales/ru.json +++ b/packages/errors/src/i18n/weblate/locales/ru.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Неверное имя пользователя или ключ восстановления.", "auth_and_registration.password_is_too_common": "Твой пароль слишком распространённый. Придумай менее предсказуемый пароль.", "auth_and_registration.password_not_set": "Пароль не установлен.", + "auth_and_registration.setup_token_invalid": "Этот токен настройки неверен. Проверьте токен и попробуйте снова.", + "auth_and_registration.setup_token_required": "Этот экземпляр еще не настроен. Завершите настройку в браузере с помощью токена настройки перед созданием учетных записей.", "auth_and_registration.username_sign_in_only": "Это доступно только в экземплярах, где вход выполняется по имени пользователя.", "bots.bot_discriminator_cannot_be_changed": "Дискриминатор бота нельзя изменить.", "bots.bot_is_private": "Этот бот приватный.", diff --git a/packages/errors/src/i18n/weblate/locales/sv-SE.json b/packages/errors/src/i18n/weblate/locales/sv-SE.json index 89a48469d..4432f4332 100644 --- a/packages/errors/src/i18n/weblate/locales/sv-SE.json +++ b/packages/errors/src/i18n/weblate/locales/sv-SE.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Fel användarnamn eller återställningsnyckel.", "auth_and_registration.password_is_too_common": "Ditt lösenord är för vanligt. Välj ett lösenord som är svårare att gissa.", "auth_and_registration.password_not_set": "Inget lösenord har angetts.", + "auth_and_registration.setup_token_invalid": "Den konfigurationstokenen är inte korrekt. Kontrollera tokenen och försök igen.", + "auth_and_registration.setup_token_required": "Den här instansen är inte konfigurerad än. Slutför konfigurationen i en webbläsare med konfigurationstokenen innan du skapar konton.", "auth_and_registration.username_sign_in_only": "Det här är bara tillgängligt på instanser där man loggar in med ett användarnamn.", "bots.bot_discriminator_cannot_be_changed": "Botdiskriminatorn kan inte ändras.", "bots.bot_is_private": "Denna bot är privat.", diff --git a/packages/errors/src/i18n/weblate/locales/th.json b/packages/errors/src/i18n/weblate/locales/th.json index 8bb984159..221581112 100644 --- a/packages/errors/src/i18n/weblate/locales/th.json +++ b/packages/errors/src/i18n/weblate/locales/th.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "ชื่อผู้ใช้หรือคีย์กู้คืนไม่ถูกต้อง", "auth_and_registration.password_is_too_common": "รหัสผ่านของคุณเป็นรหัสที่พบบ่อยเกินไป โปรดเลือกรหัสผ่านที่เดาได้ยากขึ้น", "auth_and_registration.password_not_set": "ยังไม่ได้ตั้งรหัสผ่าน", + "auth_and_registration.setup_token_invalid": "โทเค็นการตั้งค่านี้ไม่ถูกต้อง โปรดตรวจสอบโทเค็นแล้วลองอีกครั้ง", + "auth_and_registration.setup_token_required": "อินสแตนซ์นี้ยังไม่ได้ตั้งค่าให้เสร็จสิ้น โปรดตั้งค่าให้เสร็จสิ้นในเว็บเบราว์เซอร์ด้วยโทเค็นการตั้งค่าก่อนที่จะสร้างบัญชี", "auth_and_registration.username_sign_in_only": "ใช้ได้เฉพาะในอินสแตนซ์ที่ลงชื่อเข้าใช้ด้วยชื่อผู้ใช้เท่านั้น", "bots.bot_discriminator_cannot_be_changed": "ไม่สามารถเปลี่ยน discriminator ของบอทได้", "bots.bot_is_private": "บอทนี้เป็นส่วนตัว", diff --git a/packages/errors/src/i18n/weblate/locales/tr.json b/packages/errors/src/i18n/weblate/locales/tr.json index 3a21630ba..063d1741a 100644 --- a/packages/errors/src/i18n/weblate/locales/tr.json +++ b/packages/errors/src/i18n/weblate/locales/tr.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Kullanıcı adı veya kurtarma anahtarı geçersiz.", "auth_and_registration.password_is_too_common": "Şifren çok yaygın. Lütfen tahmin edilmesi daha zor bir şifre seç.", "auth_and_registration.password_not_set": "Şifre ayarlanmamış.", + "auth_and_registration.setup_token_invalid": "Bu kurulum belirteci doğru değil. Belirteci kontrol edip tekrar deneyin.", + "auth_and_registration.setup_token_required": "Bu örnek henüz kurulmadı. Hesap oluşturmadan önce kurulum belirtecini kullanarak bir web tarayıcısında kurulumu tamamlayın.", "auth_and_registration.username_sign_in_only": "Bu yalnızca kullanıcı adıyla giriş yapılan kurulumlarda kullanılabilir.", "bots.bot_discriminator_cannot_be_changed": "Bot etiketi değiştirilemez.", "bots.bot_is_private": "Bu bot özel.", diff --git a/packages/errors/src/i18n/weblate/locales/uk.json b/packages/errors/src/i18n/weblate/locales/uk.json index 62360f77b..9dcadf189 100644 --- a/packages/errors/src/i18n/weblate/locales/uk.json +++ b/packages/errors/src/i18n/weblate/locales/uk.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Неправильне ім'я користувача або ключ відновлення.", "auth_and_registration.password_is_too_common": "Твій пароль занадто поширений. Обери менш передбачуваний пароль.", "auth_and_registration.password_not_set": "Пароль не встановлено.", + "auth_and_registration.setup_token_invalid": "Цей токен налаштування неправильний. Перевірте токен і спробуйте ще раз.", + "auth_and_registration.setup_token_required": "Цей екземпляр ще не налаштовано. Завершіть налаштування у веб-браузері за допомогою токена налаштування перед створенням облікових записів.", "auth_and_registration.username_sign_in_only": "Це доступно лише на екземплярах, де вхід виконується за іменем користувача.", "bots.bot_discriminator_cannot_be_changed": "Дискримінатор бота не можна змінити.", "bots.bot_is_private": "Цей бот приватний.", diff --git a/packages/errors/src/i18n/weblate/locales/vi.json b/packages/errors/src/i18n/weblate/locales/vi.json index 4d080acc4..e8552b06f 100644 --- a/packages/errors/src/i18n/weblate/locales/vi.json +++ b/packages/errors/src/i18n/weblate/locales/vi.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Tên người dùng hoặc khóa khôi phục không hợp lệ.", "auth_and_registration.password_is_too_common": "Mật khẩu của bạn quá phổ biến. Vui lòng chọn mật khẩu khó đoán hơn.", "auth_and_registration.password_not_set": "Mật khẩu chưa được đặt.", + "auth_and_registration.setup_token_invalid": "Mã thiết lập đó không chính xác. Vui lòng kiểm tra mã và thử lại.", + "auth_and_registration.setup_token_required": "Phiên bản này chưa được thiết lập. Hãy hoàn tất thiết lập trong trình duyệt web bằng mã thiết lập trước khi tạo tài khoản.", "auth_and_registration.username_sign_in_only": "Tính năng này chỉ có trên các phiên bản đăng nhập bằng tên người dùng.", "bots.bot_discriminator_cannot_be_changed": "Không thể thay đổi mã phân biệt của bot.", "bots.bot_is_private": "Bot này ở chế độ riêng tư.", diff --git a/packages/errors/src/i18n/weblate/locales/zh-CN.json b/packages/errors/src/i18n/weblate/locales/zh-CN.json index 9cf772978..66287bf72 100644 --- a/packages/errors/src/i18n/weblate/locales/zh-CN.json +++ b/packages/errors/src/i18n/weblate/locales/zh-CN.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "用户名或恢复密钥错误。", "auth_and_registration.password_is_too_common": "你的密码太常见了。请换一个更难猜的密码。", "auth_and_registration.password_not_set": "密码未设置。", + "auth_and_registration.setup_token_invalid": "该设置令牌不正确。请检查令牌后重试。", + "auth_and_registration.setup_token_required": "此实例尚未设置。请先在网络浏览器中使用设置令牌完成设置,然后再创建帐户。", "auth_and_registration.username_sign_in_only": "仅在使用用户名登录的实例上可用。", "bots.bot_discriminator_cannot_be_changed": "机器人识别码无法更改。", "bots.bot_is_private": "此机器人是私有的。", diff --git a/packages/errors/src/i18n/weblate/locales/zh-TW.json b/packages/errors/src/i18n/weblate/locales/zh-TW.json index 7d7e77091..249433f1d 100644 --- a/packages/errors/src/i18n/weblate/locales/zh-TW.json +++ b/packages/errors/src/i18n/weblate/locales/zh-TW.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "使用者名稱或復原金鑰無效。", "auth_and_registration.password_is_too_common": "你的密碼太常見。請選擇較難預測的密碼。", "auth_and_registration.password_not_set": "密碼尚未設定。", + "auth_and_registration.setup_token_invalid": "這個設定權杖不正確。請檢查權杖後再試一次。", + "auth_and_registration.setup_token_required": "此執行個體尚未設定。請先在網頁瀏覽器中使用設定權杖完成設定,然後再建立帳號。", "auth_and_registration.username_sign_in_only": "僅適用於以使用者名稱登入的實例。", "bots.bot_discriminator_cannot_be_changed": "機器人識別碼無法變更。", "bots.bot_is_private": "此機器人為私人機器人。", diff --git a/packages/errors/src/i18n/weblate/messages.json b/packages/errors/src/i18n/weblate/messages.json index 1dbd6cdf8..8a9ba0a33 100644 --- a/packages/errors/src/i18n/weblate/messages.json +++ b/packages/errors/src/i18n/weblate/messages.json @@ -108,6 +108,8 @@ "auth_and_registration.invalid_recovery_key": "Invalid username or recovery key.", "auth_and_registration.password_is_too_common": "Your password is too common. Please choose a less predictable password.", "auth_and_registration.password_not_set": "Password isn't set.", + "auth_and_registration.setup_token_invalid": "That setup token is not correct. Check the token and try again.", + "auth_and_registration.setup_token_required": "This instance is not set up yet. Finish setup in a web browser with the setup token before creating accounts.", "auth_and_registration.username_sign_in_only": "This is only available on instances where people sign in with a username.", "bots.bot_discriminator_cannot_be_changed": "Bot discriminator can't be changed.", "bots.bot_is_private": "This bot is private.", diff --git a/packages/schema/src/domains/auth/AuthSchemas.ts b/packages/schema/src/domains/auth/AuthSchemas.ts index 1854b874a..6b41f74ed 100644 --- a/packages/schema/src/domains/auth/AuthSchemas.ts +++ b/packages/schema/src/domains/auth/AuthSchemas.ts @@ -42,6 +42,9 @@ export const RegisterRequest = z.object({ .nullish() .describe('Admin-issued registration URL code to use for this registration'), theme: RegisterThemeType.optional().describe('Initial UI theme preference for the new account'), + setup_token: createStringType(1, 256) + .nullish() + .describe('Setup token of a new self-hosted instance. Required until the first admin account exists'), }); export type RegisterRequest = z.infer; diff --git a/packages/schema/src/domains/instance/InstanceSchemas.ts b/packages/schema/src/domains/instance/InstanceSchemas.ts index ab8f3e26d..bd946b77c 100644 --- a/packages/schema/src/domains/instance/InstanceSchemas.ts +++ b/packages/schema/src/domains/instance/InstanceSchemas.ts @@ -3,7 +3,7 @@ import {AccountIdentityModes, TagStyles} from '@fluxer/constants/src/AccountIdentityConstants'; import {DomainMigrationDiscoveryResponse} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas'; import {SsoStatusResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas'; -import {createNamedStringLiteralUnion} from '@fluxer/schema/src/primitives/SchemaPrimitives'; +import {createNamedStringLiteralUnion, createStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives'; import {z} from 'zod'; const LimitFilterResponse = z.object({ @@ -56,6 +56,12 @@ export const InstanceSetupSchema = z .describe( 'Present only while a self-hosted instance is unconfigured. True when the sign-in method can no longer change', ), + setup_token_required: z + .boolean() + .optional() + .describe( + 'Present only while a self-hosted instance is unconfigured. True when registration needs the setup token', + ), }) .describe('Initial setup state for self-hosted instances'); export type InstanceSetup = z.infer; @@ -297,6 +303,9 @@ export const InstanceAccountIdentityUpdateRequest = z.object({ tag_style: TagStyleSchema.optional().describe( 'How usernames are tagged. Defaults to none. Username sign-in accepts only none', ), + setup_token: createStringType(1, 256) + .nullish() + .describe('Setup token of a new self-hosted instance. Required until the first admin account exists'), }); export type InstanceAccountIdentityUpdateRequest = z.infer; diff --git a/tools/fonts/build_fonts.py b/tools/fonts/build_fonts.py index bcd803bdb..7e672494b 100644 --- a/tools/fonts/build_fonts.py +++ b/tools/fonts/build_fonts.py @@ -610,7 +610,7 @@ def subset_options() -> SubsetOptions: options.legacy_kern = True options.layout_features = ["*"] # CJK needs vert/vrt2/halt/palt/trad/smpl options.layout_scripts = ["*"] - options.name_IDs = ["*"] # the OFL rename lives in the name table + options.name_IDs = ["*"] # the OFL rename is in the name table options.name_legacy = True options.name_languages = ["*"] options.notdef_outline = True diff --git a/tools/i18n_auto/src/runner.rs b/tools/i18n_auto/src/runner.rs index 2c6bf3980..a798b6fb2 100644 --- a/tools/i18n_auto/src/runner.rs +++ b/tools/i18n_auto/src/runner.rs @@ -1136,7 +1136,7 @@ pub fn sync_source_locale( if matches!(&config.catalog_layout, CatalogLayout::StaticJson(_)) { log( &format!( - "[{SOURCE_LOCALE}] Source strings live in the static source catalog; nothing to sync" + "[{SOURCE_LOCALE}] Source strings are in the static source catalog; nothing to sync" ), false, );