Merge commit '366856f3bcdc2ff008b04e493a5de317ab83d5d0' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2025-11-19 21:49:09 +01:00
45 changed files with 374 additions and 202 deletions

View File

@@ -24,7 +24,7 @@ gem 'ruby-vips', '~> 2.2', require: false
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8' gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.18.0', require: false gem 'bootsnap', '~> 1.19.0', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.7' gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3' gem 'chewy', '~> 7.3'

View File

@@ -129,7 +129,7 @@ GEM
binding_of_caller (1.0.1) binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0) debug_inspector (>= 1.2.0)
blurhash (0.1.8) blurhash (0.1.8)
bootsnap (1.18.6) bootsnap (1.19.0)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (7.1.1) brakeman (7.1.1)
racc racc
@@ -349,7 +349,7 @@ GEM
azure-blob (~> 0.5.2) azure-blob (~> 0.5.2)
hashie (~> 5.0) hashie (~> 5.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.15.2) json (2.16.0)
json-canonicalization (1.0.0) json-canonicalization (1.0.0)
json-jwt (1.17.0) json-jwt (1.17.0)
activesupport (>= 4.2) activesupport (>= 4.2)
@@ -446,7 +446,7 @@ GEM
mime-types-data (3.2025.0924) mime-types-data (3.2025.0924)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) mini_portile2 (2.8.9)
minitest (5.26.0) minitest (5.26.1)
msgpack (1.8.0) msgpack (1.8.0)
multi_json (1.17.0) multi_json (1.17.0)
mutex_m (0.3.0) mutex_m (0.3.0)
@@ -759,7 +759,7 @@ GEM
rubocop-ast (>= 1.47.1, < 2.0) rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.47.1) rubocop-ast (1.48.0)
parser (>= 3.3.7.2) parser (>= 3.3.7.2)
prism (~> 1.4) prism (~> 1.4)
rubocop-capybara (2.22.1) rubocop-capybara (2.22.1)
@@ -778,10 +778,10 @@ GEM
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.7.0) rubocop-rspec (3.8.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (~> 1.81)
rubocop-rspec_rails (2.31.0) rubocop-rspec_rails (2.32.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5) rubocop-rspec (~> 3.5)
@@ -941,7 +941,7 @@ DEPENDENCIES
better_errors (~> 2.9) better_errors (~> 2.9)
binding_of_caller (~> 1.0) binding_of_caller (~> 1.0)
blurhash (~> 0.1) blurhash (~> 0.1)
bootsnap (~> 1.18.0) bootsnap (~> 1.19.0)
brakeman (~> 7.0) brakeman (~> 7.0)
browser browser
bundler-audit (~> 0.9) bundler-audit (~> 0.9)

View File

@@ -9,7 +9,7 @@ module Admin
@site_upload.destroy! @site_upload.destroy!
redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') redirect_back_or_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
end end
private private

View File

@@ -180,15 +180,15 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
if (shouldHandleEvent) { if (shouldHandleEvent) {
const matchCandidates: { const matchCandidates: {
handler: (event: KeyboardEvent) => void; // A candidate will be have an undefined handler if it's matched,
// but handled in a parent component rather than this one.
handler: ((event: KeyboardEvent) => void) | undefined;
priority: number; priority: number;
}[] = []; }[] = [];
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach( (Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
(handlerName) => { (handlerName) => {
const handler = handlersRef.current[handlerName]; const handler = handlersRef.current[handlerName];
if (handler) {
const hotkeyMatcher = hotkeyMatcherMap[handlerName]; const hotkeyMatcher = hotkeyMatcherMap[handlerName];
const { isMatch, priority } = hotkeyMatcher( const { isMatch, priority } = hotkeyMatcher(
@@ -199,7 +199,6 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
if (isMatch) { if (isMatch) {
matchCandidates.push({ handler, priority }); matchCandidates.push({ handler, priority });
} }
}
}, },
); );

View File

@@ -38,7 +38,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
return ( return (
<Link <Link
className={classNames('mention hashtag', className)} className={classNames('mention hashtag', className)}
to={`/tags/${hashtag}`} to={`/tags/${encodeURIComponent(hashtag)}`}
rel='tag' rel='tag'
data-menu-hashtag={hashtagAccountId} data-menu-hashtag={hashtagAccountId}
> >
@@ -71,7 +71,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
return ( return (
<a <a
{...props} {...props}
href={encodeURI(href)} href={href}
title={href} title={href}
className={classNames('unhandled-link', className)} className={classNames('unhandled-link', className)}
target='_blank' target='_blank'

View File

@@ -13,6 +13,7 @@ import {
import { pasteLinkCompose } from 'mastodon/actions/compose_typed'; import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify'; import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify';
import { me } from 'mastodon/initial_state';
import ComposeForm from '../components/compose_form'; import ComposeForm from '../components/compose_form';
@@ -53,6 +54,7 @@ const mapStateToProps = state => ({
quoteToPrivate: quoteToPrivate:
!!state.getIn(['compose', 'quoted_status_id']) !!state.getIn(['compose', 'quoted_status_id'])
&& state.getIn(['compose', 'privacy']) === 'private' && state.getIn(['compose', 'privacy']) === 'private'
&& state.getIn(['statuses', state.getIn(['compose', 'quoted_status_id']), 'account']) !== me
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]), && !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null, isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']), lang: state.getIn(['compose', 'language']),

View File

@@ -1,6 +1,7 @@
import { initialState } from '@/mastodon/initial_state'; import { initialState } from '@/mastodon/initial_state';
import { toSupportedLocale } from './locale'; import { toSupportedLocale } from './locale';
import type { LocaleOrCustom } from './types';
import { emojiLogger } from './utils'; import { emojiLogger } from './utils';
// eslint-disable-next-line import/default -- Importing via worker loader. // eslint-disable-next-line import/default -- Importing via worker loader.
import EmojiWorker from './worker?worker&inline'; import EmojiWorker from './worker?worker&inline';
@@ -24,19 +25,17 @@ export function initializeEmoji() {
} }
if (worker) { if (worker) {
// Assign worker to const to make TS happy inside the event listener.
const thisWorker = worker;
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
log('worker is not ready after timeout'); log('worker is not ready after timeout');
worker = null; worker = null;
void fallbackLoad(); void fallbackLoad();
}, WORKER_TIMEOUT); }, WORKER_TIMEOUT);
thisWorker.addEventListener('message', (event: MessageEvent<string>) => { worker.addEventListener('message', (event: MessageEvent<string>) => {
const { data: message } = event; const { data: message } = event;
if (message === 'ready') { if (message === 'ready') {
log('worker ready, loading data'); log('worker ready, loading data');
clearTimeout(timeoutId); clearTimeout(timeoutId);
thisWorker.postMessage('custom'); messageWorker('custom');
void loadEmojiLocale(userLocale); void loadEmojiLocale(userLocale);
// Load English locale as well, because people are still used to // Load English locale as well, because people are still used to
// using it from before we supported other locales. // using it from before we supported other locales.
@@ -55,20 +54,35 @@ export function initializeEmoji() {
async function fallbackLoad() { async function fallbackLoad() {
log('falling back to main thread for loading'); log('falling back to main thread for loading');
const { importCustomEmojiData } = await import('./loader'); const { importCustomEmojiData } = await import('./loader');
await importCustomEmojiData(); const emojis = await importCustomEmojiData();
if (emojis) {
log('loaded %d custom emojis', emojis.length);
}
await loadEmojiLocale(userLocale); await loadEmojiLocale(userLocale);
if (userLocale !== 'en') { if (userLocale !== 'en') {
await loadEmojiLocale('en'); await loadEmojiLocale('en');
} }
} }
export async function loadEmojiLocale(localeString: string) { async function loadEmojiLocale(localeString: string) {
const locale = toSupportedLocale(localeString); const locale = toSupportedLocale(localeString);
const { importEmojiData, localeToPath } = await import('./loader');
if (worker) { if (worker) {
worker.postMessage(locale); const path = await localeToPath(locale);
log('asking worker to load locale %s from %s', locale, path);
messageWorker(locale, path);
} else { } else {
const { importEmojiData } = await import('./loader'); const emojis = await importEmojiData(locale);
await importEmojiData(locale); if (emojis) {
log('loaded %d emojis to locale %s', emojis.length, locale);
}
} }
} }
function messageWorker(locale: LocaleOrCustom, path?: string) {
if (!worker) {
return;
}
worker.postMessage({ locale, path });
}

View File

@@ -8,44 +8,64 @@ import {
putLatestEtag, putLatestEtag,
} from './database'; } from './database';
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
import type { CustomEmojiData, LocaleOrCustom } from './types'; import type { CustomEmojiData } from './types';
import { emojiLogger } from './utils';
const log = emojiLogger('loader'); export async function importEmojiData(localeString: string, path?: string) {
export async function importEmojiData(localeString: string) {
const locale = toSupportedLocale(localeString); const locale = toSupportedLocale(localeString);
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale);
// Validate the provided path.
if (path && !/^[/a-z]*\/packs\/assets\/compact-\w+\.json$/.test(path)) {
throw new Error('Invalid path for emoji data');
} else {
// Otherwise get the path if not provided.
path ??= await localeToPath(locale);
}
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale, path);
if (!emojis) { if (!emojis) {
return; return;
} }
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis); const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
log('loaded %d for %s locale', flattenedEmojis.length, locale);
await putEmojiData(flattenedEmojis, locale); await putEmojiData(flattenedEmojis, locale);
return flattenedEmojis;
} }
export async function importCustomEmojiData() { export async function importCustomEmojiData() {
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>('custom'); const emojis = await fetchAndCheckEtag<CustomEmojiData[]>(
'custom',
'/api/v1/custom_emojis',
);
if (!emojis) { if (!emojis) {
return; return;
} }
log('loaded %d custom emojis', emojis.length);
await putCustomEmojiData(emojis); await putCustomEmojiData(emojis);
return emojis;
} }
async function fetchAndCheckEtag<ResultType extends object[]>( const modules = import.meta.glob<string>(
localeOrCustom: LocaleOrCustom, '../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
export function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}
export async function fetchAndCheckEtag<ResultType extends object[]>(
localeString: string,
path: string,
): Promise<ResultType | null> { ): Promise<ResultType | null> {
const locale = toSupportedLocaleOrCustom(localeOrCustom); const locale = toSupportedLocaleOrCustom(localeString);
// Use location.origin as this script may be loaded from a CDN domain. // Use location.origin as this script may be loaded from a CDN domain.
const url = new URL(location.origin); const url = new URL(path, location.origin);
if (locale === 'custom') {
url.pathname = '/api/v1/custom_emojis';
} else {
const modulePath = await localeToPath(locale);
url.pathname = modulePath;
}
const oldEtag = await loadLatestEtag(locale); const oldEtag = await loadLatestEtag(locale);
const response = await fetch(url, { const response = await fetch(url, {
@@ -60,38 +80,20 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
} }
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
`Failed to fetch emoji data for ${localeOrCustom}: ${response.statusText}`, `Failed to fetch emoji data for ${locale}: ${response.statusText}`,
); );
} }
const data = (await response.json()) as ResultType; const data = (await response.json()) as ResultType;
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
throw new Error( throw new Error(`Unexpected data format for ${locale}: expected an array`);
`Unexpected data format for ${localeOrCustom}: expected an array`,
);
} }
// Store the ETag for future requests // Store the ETag for future requests
const etag = response.headers.get('ETag'); const etag = response.headers.get('ETag');
if (etag) { if (etag) {
await putLatestEtag(etag, localeOrCustom); await putLatestEtag(etag, localeString);
} }
return data; return data;
} }
const modules = import.meta.glob<string>(
'../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}

View File

@@ -162,7 +162,7 @@ describe('loadEmojiDataToState', () => {
const dbCall = vi const dbCall = vi
.spyOn(db, 'loadEmojiByHexcode') .spyOn(db, 'loadEmojiByHexcode')
.mockRejectedValue(new db.LocaleNotLoadedError('en')); .mockRejectedValue(new db.LocaleNotLoadedError('en'));
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(); vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(undefined);
const consoleCall = vi const consoleCall = vi
.spyOn(console, 'warn') .spyOn(console, 'warn')
.mockImplementationOnce(() => null); .mockImplementationOnce(() => null);

View File

@@ -1,18 +1,25 @@
import { importEmojiData, importCustomEmojiData } from './loader'; import { importCustomEmojiData, importEmojiData } from './loader';
addEventListener('message', handleMessage); addEventListener('message', handleMessage);
self.postMessage('ready'); // After the worker is ready, notify the main thread self.postMessage('ready'); // After the worker is ready, notify the main thread
function handleMessage(event: MessageEvent<string>) { function handleMessage(event: MessageEvent<{ locale: string; path?: string }>) {
const { data: locale } = event; const {
void loadData(locale); data: { locale, path },
} = event;
void loadData(locale, path);
} }
async function loadData(locale: string) { async function loadData(locale: string, path?: string) {
if (locale !== 'custom') { let importCount: number | undefined;
await importEmojiData(locale); if (locale === 'custom') {
importCount = (await importCustomEmojiData())?.length;
} else if (path) {
importCount = (await importEmojiData(locale, path))?.length;
} else { } else {
await importCustomEmojiData(); throw new Error('Path is required for loading locale emoji data');
}
if (importCount) {
self.postMessage(`loaded ${importCount} emojis into ${locale}`);
} }
self.postMessage(`loaded ${locale}`);
} }

View File

@@ -4,7 +4,7 @@
@typescript-eslint/no-unsafe-assignment */ @typescript-eslint/no-unsafe-assignment */
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { useState, useRef, useCallback } from 'react'; import { useState, useRef, useCallback, useEffect } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@@ -55,6 +55,8 @@ export const DetailedStatus: React.FC<{
pictureInPicture: any; pictureInPicture: any;
onToggleHidden?: (status: any) => void; onToggleHidden?: (status: any) => void;
onToggleMediaVisibility?: () => void; onToggleMediaVisibility?: () => void;
ancestors?: number;
multiColumn?: boolean;
}> = ({ }> = ({
status, status,
onOpenMedia, onOpenMedia,
@@ -69,6 +71,8 @@ export const DetailedStatus: React.FC<{
pictureInPicture, pictureInPicture,
onToggleMediaVisibility, onToggleMediaVisibility,
onToggleHidden, onToggleHidden,
ancestors = 0,
multiColumn = false,
}) => { }) => {
const properStatus = status?.get('reblog') ?? status; const properStatus = status?.get('reblog') ?? status;
const [height, setHeight] = useState(0); const [height, setHeight] = useState(0);
@@ -123,6 +127,30 @@ export const DetailedStatus: React.FC<{
if (onTranslate) onTranslate(status); if (onTranslate) onTranslate(status);
}, [onTranslate, status]); }, [onTranslate, status]);
// The component is managed and will change if the status changes
// Ancestors can increase when loading a thread, in which case we want to scroll,
// or decrease if a post is deleted, in which case we don't want to mess with it
const previousAncestors = useRef(-1);
useEffect(() => {
if (nodeRef.current && previousAncestors.current < ancestors) {
nodeRef.current.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header, so compensate for that.
if (!multiColumn) {
const offset = document
.querySelector('.column-header__wrapper')
?.getBoundingClientRect().bottom;
if (offset) {
const scrollingElement = document.scrollingElement ?? document.body;
scrollingElement.scrollBy(0, -offset);
}
}
}
previousAncestors.current = ancestors;
}, [ancestors, multiColumn]);
if (!properStatus) { if (!properStatus) {
return null; return null;
} }

View File

@@ -164,8 +164,6 @@ class Status extends ImmutablePureComponent {
componentDidMount () { componentDidMount () {
attachFullscreenListener(this.onFullScreenChange); attachFullscreenListener(this.onFullScreenChange);
this._scrollStatusIntoView();
} }
UNSAFE_componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
@@ -487,35 +485,11 @@ class Status extends ImmutablePureComponent {
this.statusNode = c; this.statusNode = c;
}; };
_scrollStatusIntoView () {
const { status, multiColumn } = this.props;
if (status) {
requestIdleCallback(() => {
this.statusNode?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that.
if (!multiColumn) {
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
if (offset) {
const scrollingElement = document.scrollingElement || document.body;
scrollingElement.scrollBy(0, -offset);
}
}
});
}
}
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, descendantsIds } = this.props;
const isSameStatus = status && (prevProps.status?.get('id') === status.get('id')); const isSameStatus = status && (prevProps.status?.get('id') === status.get('id'));
if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || !isSameStatus)) {
this._scrollStatusIntoView();
}
// Only highlight replies after the initial load // Only highlight replies after the initial load
if (prevProps.descendantsIds.length && isSameStatus) { if (prevProps.descendantsIds.length && isSameStatus) {
const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds); const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds);
@@ -619,6 +593,8 @@ class Status extends ImmutablePureComponent {
showMedia={this.state.showMedia} showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture} pictureInPicture={pictureInPicture}
ancestors={this.props.ancestorsIds.length}
multiColumn={multiColumn}
/> />
<ActionBar <ActionBar

View File

@@ -903,6 +903,7 @@
"status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}", "status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}",
"status.embed": "Získejte kód pro vložení", "status.embed": "Získejte kód pro vložení",
"status.favourite": "Oblíbit", "status.favourite": "Oblíbit",
"status.favourites_count": "{count, plural, one {{counter} oblíbený} few {{counter} oblíbené} many {{counter} oblíbených} other {{counter} oblíbených}}",
"status.filter": "Filtrovat tento příspěvek", "status.filter": "Filtrovat tento příspěvek",
"status.history.created": "Uživatel {name} vytvořil {date}", "status.history.created": "Uživatel {name} vytvořil {date}",
"status.history.edited": "Uživatel {name} upravil {date}", "status.history.edited": "Uživatel {name} upravil {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Tento příspěvek zatím nikdo necitoval. Pokud tak někdo učiní, uvidíte to zde.", "status.quotes.empty": "Tento příspěvek zatím nikdo necitoval. Pokud tak někdo učiní, uvidíte to zde.",
"status.quotes.local_other_disclaimer": "Citace zamítnuté autorem nebudou zobrazeny.", "status.quotes.local_other_disclaimer": "Citace zamítnuté autorem nebudou zobrazeny.",
"status.quotes.remote_other_disclaimer": "Pouze citace z {domain} zde budou zaručeně ukázány. Citace zamítnuté autorem nebudou zobrazeny.", "status.quotes.remote_other_disclaimer": "Pouze citace z {domain} zde budou zaručeně ukázány. Citace zamítnuté autorem nebudou zobrazeny.",
"status.quotes_count": "{count, plural, one {{counter} citace} few {{counter} citace} many {{counter} citací} other {{counter} citací}}",
"status.read_more": "Číst více", "status.read_more": "Číst více",
"status.reblog": "Boostnout", "status.reblog": "Boostnout",
"status.reblog_or_quote": "Boostnout nebo citovat", "status.reblog_or_quote": "Boostnout nebo citovat",
"status.reblog_private": "Sdílejte znovu se svými sledujícími", "status.reblog_private": "Sdílejte znovu se svými sledujícími",
"status.reblogged_by": "Uživatel {name} boostnul", "status.reblogged_by": "Uživatel {name} boostnul",
"status.reblogs.empty": "Tento příspěvek ještě nikdo neboostnul. Pokud to někdo udělá, zobrazí se zde.", "status.reblogs.empty": "Tento příspěvek ještě nikdo neboostnul. Pokud to někdo udělá, zobrazí se zde.",
"status.reblogs_count": "{count, plural, one {{counter} boost} few {{counter} boosty} many {{counter} boostů} other {{counter} boostů}}",
"status.redraft": "Smazat a přepsat", "status.redraft": "Smazat a přepsat",
"status.remove_bookmark": "Odstranit ze záložek", "status.remove_bookmark": "Odstranit ze záložek",
"status.remove_favourite": "Odebrat z oblíbených", "status.remove_favourite": "Odebrat z oblíbených",

View File

@@ -903,6 +903,7 @@
"status.edited_x_times": "Golygwyd {count, plural, one {{count} gwaith} other {{count} gwaith}}", "status.edited_x_times": "Golygwyd {count, plural, one {{count} gwaith} other {{count} gwaith}}",
"status.embed": "Cael y cod mewnblannu", "status.embed": "Cael y cod mewnblannu",
"status.favourite": "Ffafrio", "status.favourite": "Ffafrio",
"status.favourites_count": "{count, plural, one {{counter} ffefryn} other {{counter} ffefryn}}",
"status.filter": "Hidlo'r postiad hwn", "status.filter": "Hidlo'r postiad hwn",
"status.history.created": "Crëwyd gan {name} {date}", "status.history.created": "Crëwyd gan {name} {date}",
"status.history.edited": "Golygwyd gan {name} {date}", "status.history.edited": "Golygwyd gan {name} {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Does neb wedi dyfynnu'r postiad hwn eto. Pan fydd rhywun yn gwneud hynny, bydd yn ymddangos yma.", "status.quotes.empty": "Does neb wedi dyfynnu'r postiad hwn eto. Pan fydd rhywun yn gwneud hynny, bydd yn ymddangos yma.",
"status.quotes.local_other_disclaimer": "Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.", "status.quotes.local_other_disclaimer": "Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.",
"status.quotes.remote_other_disclaimer": "Dim ond dyfyniadau o {domain} sy'n siŵr o gael eu dangos yma. Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.", "status.quotes.remote_other_disclaimer": "Dim ond dyfyniadau o {domain} sy'n siŵr o gael eu dangos yma. Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.",
"status.quotes_count": "{count, plural, one {{counter} dyfyniad} other {{counter} dyfyniad}}",
"status.read_more": "Darllen rhagor", "status.read_more": "Darllen rhagor",
"status.reblog": "Hybu", "status.reblog": "Hybu",
"status.reblog_or_quote": "Hybu neu ddyfynnu", "status.reblog_or_quote": "Hybu neu ddyfynnu",
"status.reblog_private": "Rhannwch eto gyda'ch dilynwyr", "status.reblog_private": "Rhannwch eto gyda'ch dilynwyr",
"status.reblogged_by": "Hybodd {name}", "status.reblogged_by": "Hybodd {name}",
"status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.", "status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
"status.reblogs_count": "{count, plural, one {{counter} hwb} other {{counter} hwb}}",
"status.redraft": "Dileu ac ail lunio", "status.redraft": "Dileu ac ail lunio",
"status.remove_bookmark": "Tynnu nod tudalen", "status.remove_bookmark": "Tynnu nod tudalen",
"status.remove_favourite": "Tynnu o'r ffefrynnau", "status.remove_favourite": "Tynnu o'r ffefrynnau",

View File

@@ -231,7 +231,7 @@
"confirmations.delete_list.title": "Slet liste?", "confirmations.delete_list.title": "Slet liste?",
"confirmations.discard_draft.confirm": "Kassér og fortsæt", "confirmations.discard_draft.confirm": "Kassér og fortsæt",
"confirmations.discard_draft.edit.cancel": "Fortsæt redigering", "confirmations.discard_draft.edit.cancel": "Fortsæt redigering",
"confirmations.discard_draft.edit.message": "Hvis du fortsætter, kasseres alle ændringer, du har foretaget i det indlæg, du er i gang med at redigere.", "confirmations.discard_draft.edit.message": "Hvis du fortsætter, vil alle ændringer, du har foretaget i det indlæg, du er er ved at redigere, blive slettet.",
"confirmations.discard_draft.edit.title": "Kassér ændringer til dit indlæg?", "confirmations.discard_draft.edit.title": "Kassér ændringer til dit indlæg?",
"confirmations.discard_draft.post.cancel": "Genoptag udkast", "confirmations.discard_draft.post.cancel": "Genoptag udkast",
"confirmations.discard_draft.post.message": "Hvis du fortsætter, kasseres det indlæg, du er i gang med at udforme.", "confirmations.discard_draft.post.message": "Hvis du fortsætter, kasseres det indlæg, du er i gang med at udforme.",
@@ -507,7 +507,7 @@
"keyboard_shortcuts.pinned": "Åbn liste over fastgjorte indlæg", "keyboard_shortcuts.pinned": "Åbn liste over fastgjorte indlæg",
"keyboard_shortcuts.profile": "Åbn forfatters profil", "keyboard_shortcuts.profile": "Åbn forfatters profil",
"keyboard_shortcuts.quote": "Citér indlæg", "keyboard_shortcuts.quote": "Citér indlæg",
"keyboard_shortcuts.reply": "Besvar indlægget", "keyboard_shortcuts.reply": "Besvar indlæg",
"keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger", "keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger",
"keyboard_shortcuts.search": "Fokusér søgebjælke", "keyboard_shortcuts.search": "Fokusér søgebjælke",
"keyboard_shortcuts.spoilers": "Vis/skjul indholdsadvarsel-felt", "keyboard_shortcuts.spoilers": "Vis/skjul indholdsadvarsel-felt",
@@ -675,7 +675,7 @@
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke", "notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
"notifications.column_settings.follow": "Nye følgere:", "notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.follow_request": "Nye følgeanmodninger:", "notifications.column_settings.follow_request": "Nye følgeanmodninger:",
"notifications.column_settings.group": "Gruppere", "notifications.column_settings.group": "Gruppér",
"notifications.column_settings.mention": "Omtaler:", "notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Afstemningsresultater:", "notifications.column_settings.poll": "Afstemningsresultater:",
"notifications.column_settings.push": "Push-notifikationer", "notifications.column_settings.push": "Push-notifikationer",
@@ -764,7 +764,7 @@
"privacy_policy.last_updated": "Senest opdateret {date}", "privacy_policy.last_updated": "Senest opdateret {date}",
"privacy_policy.title": "Privatlivspolitik", "privacy_policy.title": "Privatlivspolitik",
"quote_error.edit": "Citater kan ikke tilføjes ved redigering af et indlæg.", "quote_error.edit": "Citater kan ikke tilføjes ved redigering af et indlæg.",
"quote_error.poll": "Citering ikke tilladt i afstemninger.", "quote_error.poll": "Citering er ikke tilladt med afstemninger.",
"quote_error.private_mentions": "Citering er ikke tilladt med direkte omtaler.", "quote_error.private_mentions": "Citering er ikke tilladt med direkte omtaler.",
"quote_error.quote": "Kun ét citat ad gangen er tilladt.", "quote_error.quote": "Kun ét citat ad gangen er tilladt.",
"quote_error.unauthorized": "Du har ikke tilladelse til at citere dette indlæg.", "quote_error.unauthorized": "Du har ikke tilladelse til at citere dette indlæg.",
@@ -867,7 +867,7 @@
"search_results.title": "Søg efter \"{q}\"", "search_results.title": "Søg efter \"{q}\"",
"server_banner.about_active_users": "Personer, som brugte denne server de seneste 30 dage (månedlige aktive brugere)", "server_banner.about_active_users": "Personer, som brugte denne server de seneste 30 dage (månedlige aktive brugere)",
"server_banner.active_users": "aktive brugere", "server_banner.active_users": "aktive brugere",
"server_banner.administered_by": "Håndteres af:", "server_banner.administered_by": "Administreret af:",
"server_banner.is_one_of_many": "{domain} er en af de mange uafhængige Mastodon-servere, du kan bruge for at deltage i fediverset.", "server_banner.is_one_of_many": "{domain} er en af de mange uafhængige Mastodon-servere, du kan bruge for at deltage i fediverset.",
"server_banner.server_stats": "Serverstatstik:", "server_banner.server_stats": "Serverstatstik:",
"sign_in_banner.create_account": "Opret konto", "sign_in_banner.create_account": "Opret konto",
@@ -902,7 +902,7 @@
"status.edited": "Senest redigeret {date}", "status.edited": "Senest redigeret {date}",
"status.edited_x_times": "Redigeret {count, plural, one {{count} gang} other {{count} gange}}", "status.edited_x_times": "Redigeret {count, plural, one {{count} gang} other {{count} gange}}",
"status.embed": "Hent indlejringskode", "status.embed": "Hent indlejringskode",
"status.favourite": "Favorit", "status.favourite": "Favoritmarkér",
"status.favourites_count": "{count, plural, one {{counter} favorit} other {{counter} favoritter}}", "status.favourites_count": "{count, plural, one {{counter} favorit} other {{counter} favoritter}}",
"status.filter": "Filtrér dette indlæg", "status.filter": "Filtrér dette indlæg",
"status.history.created": "{name} oprettet {date}", "status.history.created": "{name} oprettet {date}",
@@ -991,7 +991,7 @@
"units.short.million": "{count} mio.", "units.short.million": "{count} mio.",
"units.short.thousand": "{count} tusind", "units.short.thousand": "{count} tusind",
"upload_area.title": "Træk og slip for at uploade", "upload_area.title": "Træk og slip for at uploade",
"upload_button.label": "Tilføj billed-, video- eller lydfil(er)", "upload_button.label": "Tilføj billeder, en video- eller lydfil",
"upload_error.limit": "Grænse for filupload nået.", "upload_error.limit": "Grænse for filupload nået.",
"upload_error.poll": "Filupload ikke tilladt for afstemninger.", "upload_error.poll": "Filupload ikke tilladt for afstemninger.",
"upload_error.quote": "Fil-upload ikke tilladt i citater.", "upload_error.quote": "Fil-upload ikke tilladt i citater.",

View File

@@ -242,7 +242,7 @@
"confirmations.follow_to_list.message": "Du musst {name} folgen, um das Profil zu einer Liste hinzufügen zu können.", "confirmations.follow_to_list.message": "Du musst {name} folgen, um das Profil zu einer Liste hinzufügen zu können.",
"confirmations.follow_to_list.title": "Profil folgen?", "confirmations.follow_to_list.title": "Profil folgen?",
"confirmations.logout.confirm": "Abmelden", "confirmations.logout.confirm": "Abmelden",
"confirmations.logout.message": "Möchtest du dich wirklich abmelden?", "confirmations.logout.message": "Bist du sicher, dass du dich abmelden möchtest?",
"confirmations.logout.title": "Abmelden?", "confirmations.logout.title": "Abmelden?",
"confirmations.missing_alt_text.confirm": "Bildbeschreibung hinzufügen", "confirmations.missing_alt_text.confirm": "Bildbeschreibung hinzufügen",
"confirmations.missing_alt_text.message": "Dein Beitrag enthält Medien ohne Bildbeschreibung. Mit ALT-Texten erreichst Du auch Menschen, die blind oder sehbehindert sind.", "confirmations.missing_alt_text.message": "Dein Beitrag enthält Medien ohne Bildbeschreibung. Mit ALT-Texten erreichst Du auch Menschen, die blind oder sehbehindert sind.",
@@ -584,8 +584,8 @@
"navigation_bar.follows_and_followers": "Follower und Folge ich", "navigation_bar.follows_and_followers": "Follower und Folge ich",
"navigation_bar.import_export": "Importieren und exportieren", "navigation_bar.import_export": "Importieren und exportieren",
"navigation_bar.lists": "Listen", "navigation_bar.lists": "Listen",
"navigation_bar.live_feed_local": "Live-Feed (lokal)", "navigation_bar.live_feed_local": "Live-Feed (Dieser Server)",
"navigation_bar.live_feed_public": "Live-Feed (öffentlich)", "navigation_bar.live_feed_public": "Live-Feed (Alle Server)",
"navigation_bar.logout": "Abmelden", "navigation_bar.logout": "Abmelden",
"navigation_bar.moderation": "Moderation", "navigation_bar.moderation": "Moderation",
"navigation_bar.more": "Mehr", "navigation_bar.more": "Mehr",

View File

@@ -233,7 +233,7 @@
"confirmations.discard_draft.edit.cancel": "Palaa muokkaamaan", "confirmations.discard_draft.edit.cancel": "Palaa muokkaamaan",
"confirmations.discard_draft.edit.message": "Jatkaminen tuhoaa kaikki muutokset, joita olet tehnyt julkaisuun, jota olet parhaillaan muokkaamassa.", "confirmations.discard_draft.edit.message": "Jatkaminen tuhoaa kaikki muutokset, joita olet tehnyt julkaisuun, jota olet parhaillaan muokkaamassa.",
"confirmations.discard_draft.edit.title": "Hylätäänkö luonnosjulkaisusi muutokset?", "confirmations.discard_draft.edit.title": "Hylätäänkö luonnosjulkaisusi muutokset?",
"confirmations.discard_draft.post.cancel": "Palaa lunnokseen", "confirmations.discard_draft.post.cancel": "Palaa luonnokseen",
"confirmations.discard_draft.post.message": "Jatkaminen tuhoaa julkaisun, jota olet parhaillaan laatimassa.", "confirmations.discard_draft.post.message": "Jatkaminen tuhoaa julkaisun, jota olet parhaillaan laatimassa.",
"confirmations.discard_draft.post.title": "Hylätäänkö luonnosjulkaisusi?", "confirmations.discard_draft.post.title": "Hylätäänkö luonnosjulkaisusi?",
"confirmations.discard_edit_media.confirm": "Hylkää", "confirmations.discard_edit_media.confirm": "Hylkää",

View File

@@ -528,7 +528,7 @@
"limited_account_hint.action": "Afficher le profil quand même", "limited_account_hint.action": "Afficher le profil quand même",
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.", "limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"link_preview.author": "Par {name}", "link_preview.author": "Par {name}",
"link_preview.more_from_author": "Plus via {name}", "link_preview.more_from_author": "Voir plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}", "link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
"lists.add_member": "Ajouter", "lists.add_member": "Ajouter",
"lists.add_to_list": "Ajouter à la liste", "lists.add_to_list": "Ajouter à la liste",

View File

@@ -528,7 +528,7 @@
"limited_account_hint.action": "Afficher le profil quand même", "limited_account_hint.action": "Afficher le profil quand même",
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.", "limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"link_preview.author": "Par {name}", "link_preview.author": "Par {name}",
"link_preview.more_from_author": "Plus via {name}", "link_preview.more_from_author": "Voir plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}", "link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
"lists.add_member": "Ajouter", "lists.add_member": "Ajouter",
"lists.add_to_list": "Ajouter à la liste", "lists.add_to_list": "Ajouter à la liste",

View File

@@ -189,7 +189,7 @@
"notification.update": "{name} nuntium correxit", "notification.update": "{name} nuntium correxit",
"notification_requests.accept": "Accipe", "notification_requests.accept": "Accipe",
"notification_requests.confirm_accept_multiple.message": "Tu es accepturus {count, plural, one {una notitia petitionem} other {# notitia petitiones}}. Certus esne procedere vis?", "notification_requests.confirm_accept_multiple.message": "Tu es accepturus {count, plural, one {una notitia petitionem} other {# notitia petitiones}}. Certus esne procedere vis?",
"notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {Illa} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?", "notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {it} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?",
"notifications.filter.all": "Omnia", "notifications.filter.all": "Omnia",
"notifications.filter.polls": "Eventus electionis", "notifications.filter.polls": "Eventus electionis",
"notifications.group": "{count} Notificātiōnēs", "notifications.group": "{count} Notificātiōnēs",
@@ -246,19 +246,102 @@
"status.history.created": "{name} creatum {date}", "status.history.created": "{name} creatum {date}",
"status.history.edited": "{name} correxit {date}", "status.history.edited": "{name} correxit {date}",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.quotes.empty": "Nemo hanc commentationem adhuc citavit. Cum quis citaverit, hic apparebit.",
"status.quotes.local_other_disclaimer": "Citationes ab auctore reiec­tæ non monstrabuntur.",
"status.quotes.remote_other_disclaimer": "Tantum citae ex {domain} hic exhiberi praestantur. Citae ab auctore reiectae non exhibebuntur.",
"status.quotes_count": "{count, plural, one {{counter} citatio} other {{counter} citationes}}",
"status.read_more": "Plura lege",
"status.reblog": "Promovere",
"status.reblog_or_quote": "Promovere aut cita",
"status.reblog_private": "Iterum cum sectatoribus tuis communica",
"status.reblogged_by": "{name} adiuvavit", "status.reblogged_by": "{name} adiuvavit",
"status.reblogs.empty": "Nemo hanc publicationem adhuc promovit. Cum quis eam promoveat, hic apparebunt.",
"status.reblogs_count": "{count, plural, one {{counter} incrementum} other {{counter} incrementa}}",
"status.redraft": "Dele et redig",
"status.remove_bookmark": "Tolle signum",
"status.remove_favourite": "Tolle ad delectis",
"status.remove_quote": "Tolle",
"status.replied_in_thread": "In filo responsum",
"status.replied_to": "{name} respondit",
"status.reply": "Respondere",
"status.replyAll": "Responde ad filum",
"status.report": "Referre @{name}",
"status.request_quote": "Pretium petere",
"status.revoke_quote": "Tolle nuntium meum ex nuntio @{name}",
"status.sensitive_warning": "Materia delicata",
"status.share": "Communica",
"status.show_less_all": "Omnibus minus monstra",
"status.show_more_all": "Omnibus plura monstra",
"status.show_original": "Monstra originalem",
"status.title.with_attachments": "{user} publicavit {attachmentCount, plural, one {unum annexum} other {{attachmentCount} annexa}}", "status.title.with_attachments": "{user} publicavit {attachmentCount, plural, one {unum annexum} other {{attachmentCount} annexa}}",
"status.translate": "Converte",
"status.translated_from_with": "Translatum ex {lang} per {provider}",
"status.uncached_media_warning": "Praevisum non praesto est",
"status.unmute_conversation": "Conversationem reserare",
"subscribed_languages.lead": "Tantum epistolae in linguis selectis in domo tua apparebunt et indices temporum post mutationem. Neminem eligatis qui epistolas in omnibus linguis recipiat.",
"subscribed_languages.save": "Servare mutationes",
"subscribed_languages.target": "Muta linguas subscriptas pro {target}",
"tabs_bar.home": "Domi", "tabs_bar.home": "Domi",
"tabs_bar.menu": "Elenchus",
"tabs_bar.notifications": "Acta Vicimediorum",
"tabs_bar.publish": "Nova publicatio",
"tabs_bar.search": "Quaere",
"terms_of_service.effective_as_of": "Valet ex {date}",
"terms_of_service.title": "Termini servitii",
"terms_of_service.upcoming_changes_on": "Mutationes venturae die {date}",
"time_remaining.days": "{number, plural, one {# die} other {# dies}} restant", "time_remaining.days": "{number, plural, one {# die} other {# dies}} restant",
"time_remaining.hours": "{number, plural, one {# hora} other {# horae}} restant", "time_remaining.hours": "{number, plural, one {# hora} other {# horae}} restant",
"time_remaining.minutes": "{number, plural, one {# minutum} other {# minuta}} restant", "time_remaining.minutes": "{number, plural, one {# minutum} other {# minuta}} restant",
"time_remaining.moments": "Momenta reliqua",
"time_remaining.seconds": "{number, plural, one {# secundum} other {# secunda}} restant", "time_remaining.seconds": "{number, plural, one {# secundum} other {# secunda}} restant",
"trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {diē prīdiē} other {diēbus praeteritīs {days}}}", "trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {days} other {diēbus praeteritīs {days}}}",
"trends.trending_now": "Nunc in usu",
"ui.beforeunload": "Si Mastodon discesseris, tua epitome peribit.", "ui.beforeunload": "Si Mastodon discesseris, tua epitome peribit.",
"units.short.billion": "{count} millia milionum", "units.short.billion": "{count} millia milionum",
"units.short.million": "{count} milionum", "units.short.million": "{count} milionum",
"units.short.thousand": "{count} millia", "units.short.thousand": "{count} millia",
"upload_button.label": "Imaginēs, vīdeō aut fīle audītūs adde", "upload_area.title": "Trahe et depone ad imponendum",
"upload_button.label": "Adde imagines, pelliculam, aut fasciculum sonorum.",
"upload_error.limit": "Limes onerationis superatus est.",
"upload_error.poll": "Nullis suffragiis licet fascicula imponere.",
"upload_error.quote": "Nullum oneramentum fasciculi cum citationibus permittitur.",
"upload_form.drag_and_drop.instructions": "Ad annexum mediorum tollendum, preme clavem \"Space\" aut \"Enter\". Dum traheis, utere clavibus sagittariis ad annexum mediorum in quamlibet partem movendum. Preme iterum \"Space\" aut \"Enter\" ad annexum mediorum in novo loco deponendum, aut preme \"Escape\" ad desinendum.",
"upload_form.drag_and_drop.on_drag_cancel": "Tractatio revocata est. Adiunctum medium {item} demissum est.",
"upload_form.drag_and_drop.on_drag_end": "Adhaesum medium {item} demissum est.",
"upload_form.drag_and_drop.on_drag_over": "Adhaesum medium {item} motum est.",
"upload_form.drag_and_drop.on_drag_start": "Adhaesum medium {item} sublatum est.",
"upload_form.edit": "Recolere", "upload_form.edit": "Recolere",
"upload_progress.label": "Uploading…" "upload_progress.label": "Oneratur...",
"upload_progress.processing": "Processus…",
"username.taken": "Illud nomen usoris occupatum est. Aliud tenta.",
"video.close": "Pelliculam claude",
"video.download": "Prehendere fasciculus",
"video.exit_fullscreen": "Exitus ex plenum monitorium",
"video.expand": "Expande pelliculam",
"video.fullscreen": "Plenum monitorium",
"video.hide": "celare pellicula",
"video.mute": "Mutus",
"video.pause": "intermittere",
"video.play": "gignere",
"video.skip_backward": "Redire",
"video.skip_forward": "Progredi",
"video.unmute": "Sordes tollere",
"video.volume_down": "Volumen deminui",
"video.volume_up": "Volumen augete",
"visibility_modal.button_title": "Visibilitatem statuere",
"visibility_modal.direct_quote_warning.text": "Si praesentia configuramenta servaveris, sententia inserta in nexum convertetur.",
"visibility_modal.direct_quote_warning.title": "Citatio in mentitionibus privatis inseriri non possunt.",
"visibility_modal.header": "Visibilitas et interactio",
"visibility_modal.helper.direct_quoting": "Mentiones privatae in Mastodon scriptae ab aliis citari non possunt.",
"visibility_modal.helper.privacy_editing": "Visibilitas mutari non potest postquam nuntius publicatus est.",
"visibility_modal.helper.privacy_private_self_quote": "Citationes propriae nuntiorum privatorum publicari non possunt.",
"visibility_modal.helper.private_quoting": "Nuntii in Mastodon a sequacibus tantum scriptī ab aliis citari non possunt.",
"visibility_modal.helper.unlisted_quoting": "Cum te citant, eorum scriptum etiam ex indicibus popularibus celabitur.",
"visibility_modal.instructions": "Régula quis cum hoc scripto agere possit. Potes etiam ordinationes omnibus scriptis futuris adhibere, navigando ad <link>Praeferentias > Regulas scriptionis praedefinitas</link>.",
"visibility_modal.privacy_label": "Visibilitas",
"visibility_modal.quote_followers": "Sectatores tantum",
"visibility_modal.quote_label": "Quis citare potest",
"visibility_modal.quote_nobody": "Sicut me",
"visibility_modal.quote_public": "quisquis",
"visibility_modal.save": "Servare"
} }

View File

@@ -9,7 +9,6 @@ import { me, reduceMotion } from 'mastodon/initial_state';
import ready from 'mastodon/ready'; import ready from 'mastodon/ready';
import { store } from 'mastodon/store'; import { store } from 'mastodon/store';
import { initializeEmoji } from './features/emoji';
import { isProduction, isDevelopment } from './utils/environment'; import { isProduction, isDevelopment } from './utils/environment';
function main() { function main() {
@@ -30,6 +29,7 @@ function main() {
}); });
} }
const { initializeEmoji } = await import('./features/emoji/index');
initializeEmoji(); initializeEmoji();
const root = createRoot(mountNode); const root = createRoot(mountNode);

View File

@@ -341,8 +341,8 @@ export const composeReducer = (state = initialState, action) => {
const isDirect = state.get('privacy') === 'direct'; const isDirect = state.get('privacy') === 'direct';
return state return state
.set('quoted_status_id', isDirect ? null : status.get('id')) .set('quoted_status_id', isDirect ? null : status.get('id'))
.set('spoiler', status.get('sensitive')) .update('spoiler', spoiler => (spoiler) || !!status.get('spoiler_text'))
.set('spoiler_text', status.get('spoiler_text')) .update('spoiler_text', (spoiler_text) => spoiler_text || status.get('spoiler_text'))
.update('privacy', (visibility) => { .update('privacy', (visibility) => {
if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') { if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') {
return 'private'; return 'private';

View File

@@ -327,9 +327,9 @@ $content-width: 840px;
font-weight: 700; font-weight: 700;
color: $primary-text-color; color: $primary-text-color;
text-transform: none; text-transform: none;
padding-bottom: 0; padding-top: 0;
margin-bottom: 0; margin-bottom: 0;
border-bottom: 0; border-top: 0;
.comment { .comment {
display: block; display: block;

View File

@@ -319,9 +319,9 @@ $content-width: 840px;
font-weight: 700; font-weight: 700;
color: var(--color-text-primary); color: var(--color-text-primary);
text-transform: none; text-transform: none;
padding-bottom: 0; padding-top: 0;
margin-bottom: 0; margin-bottom: 0;
border-bottom: 0; border-top: 0;
.comment { .comment {
display: block; display: block;

View File

@@ -31,9 +31,10 @@ class IpBlock < ApplicationRecord
after_commit :reset_cache after_commit :reset_cache
def to_log_human_identifier def to_cidr
"#{ip}/#{ip.prefix}" "#{ip}/#{ip.prefix}"
end end
alias to_log_human_identifier to_cidr
class << self class << self
def blocked?(remote_ip) def blocked?(remote_ip)

View File

@@ -9,6 +9,6 @@ class REST::Admin::IpBlockSerializer < ActiveModel::Serializer
end end
def ip def ip
"#{object.ip}/#{object.ip.prefix}" object.to_cidr
end end
end end

View File

@@ -3,7 +3,7 @@
= f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id = f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id
.batch-table__row__content.pending-account .batch-table__row__content.pending-account
.pending-account__header .pending-account__header
%samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}") %samp= link_to ip_block.to_cidr, admin_accounts_path(ip: ip_block.to_cidr)
- if ip_block.comment.present? - if ip_block.comment.present?
· ·
= ip_block.comment = ip_block.comment

View File

@@ -477,7 +477,7 @@ da:
no_file: Ingen fil valgt no_file: Ingen fil valgt
export_domain_blocks: export_domain_blocks:
import: import:
description_html: En liste over domæneblokeringer er ved at blive importeret. Gennemgå listen meget nøje, især hvis man ikke selv har oprettet den. description_html: Du er ved at importere en liste over domæneblokeringer. Gennemgå denne liste meget omhyggeligt, især hvis du ikke selv har udarbejdet den.
existing_relationships_warning: Eksisterende følge-relationer existing_relationships_warning: Eksisterende følge-relationer
private_comment_description_html: 'For at man lettere kan holde styr på, hvorfra importerede blokeringer kommer, oprettes disse med flg. private kommentar: <q>%{comment}</q>' private_comment_description_html: 'For at man lettere kan holde styr på, hvorfra importerede blokeringer kommer, oprettes disse med flg. private kommentar: <q>%{comment}</q>'
private_comment_template: Importeret fra %{source} d. %{date} private_comment_template: Importeret fra %{source} d. %{date}
@@ -1186,7 +1186,7 @@ da:
new_trending_statuses: new_trending_statuses:
title: Indlæg, der trender title: Indlæg, der trender
new_trending_tags: new_trending_tags:
title: Hashtags, der trender title: Populære hashtags
subject: Nye tendenser klar til gennemgang på %{instance} subject: Nye tendenser klar til gennemgang på %{instance}
aliases: aliases:
add_new: Opret alias add_new: Opret alias

View File

@@ -1089,16 +1089,16 @@ de:
tag_servers_measure: Server tag_servers_measure: Server
tag_uses_measure: insgesamt tag_uses_measure: insgesamt
description_html: Diese Hashtags werden derzeit in vielen Beiträgen verwendet, die dein Server sieht. Dies kann deinen Nutzer*innen helfen, herauszufinden, worüber die Leute im Moment am meisten schreiben. Hashtags werden erst dann öffentlich angezeigt, wenn du sie genehmigst. description_html: Diese Hashtags werden derzeit in vielen Beiträgen verwendet, die dein Server sieht. Dies kann deinen Nutzer*innen helfen, herauszufinden, worüber die Leute im Moment am meisten schreiben. Hashtags werden erst dann öffentlich angezeigt, wenn du sie genehmigst.
listable: Kann vorgeschlagen werden listable: Darf empfohlen werden
no_tag_selected: Keine Hashtags wurden geändert, da keine ausgewählt wurden no_tag_selected: Keine Hashtags wurden geändert, da keine ausgewählt wurden
not_listable: Wird nicht vorgeschlagen not_listable: Darf nicht vorgeschlagen werden
not_trendable: Wird in den Trends nicht angezeigt not_trendable: In Trends nicht erlaubt
not_usable: Kann nicht verwendet werden not_usable: In Beiträgen nicht erlaubt
peaked_on_and_decaying: In den Trends am %{date}, jetzt absteigend peaked_on_and_decaying: In den Trends am %{date}, jetzt absteigend
title: Angesagte Hashtags title: Angesagte Hashtags
trendable: Darf in den Trends erscheinen trendable: In Trends erlaubt
trending_rank: Platz %{rank} trending_rank: Platz %{rank}
usable: Darf verwendet werden usable: In Beiträgen erlaubt
usage_comparison: Heute %{today}-mal und gestern %{yesterday}-mal verwendet usage_comparison: Heute %{today}-mal und gestern %{yesterday}-mal verwendet
used_by_over_week: used_by_over_week:
one: In den vergangenen 7 Tagen von einem Profil verwendet one: In den vergangenen 7 Tagen von einem Profil verwendet
@@ -1122,7 +1122,7 @@ de:
title: Neue Regel für Profilnamen erstellen title: Neue Regel für Profilnamen erstellen
no_username_block_selected: Keine Regeln für Profilnamen wurden geändert, weil keine ausgewählt wurde(n) no_username_block_selected: Keine Regeln für Profilnamen wurden geändert, weil keine ausgewählt wurde(n)
not_permitted: Nicht gestattet not_permitted: Nicht gestattet
title: Regeln für Profilnamen title: Profilnamen
updated_msg: Regel für Profilnamen erfolgreich aktualisiert updated_msg: Regel für Profilnamen erfolgreich aktualisiert
warning_presets: warning_presets:
add_new: Neu hinzufügen add_new: Neu hinzufügen
@@ -1285,7 +1285,7 @@ de:
new_confirmation_instructions_sent: In wenigen Minuten wirst du eine neue E-Mail mit dem Bestätigungslink erhalten! new_confirmation_instructions_sent: In wenigen Minuten wirst du eine neue E-Mail mit dem Bestätigungslink erhalten!
title: Überprüfe dein E-Mail-Postfach title: Überprüfe dein E-Mail-Postfach
sign_in: sign_in:
preamble_html: Melde dich mit deinen Zugangsdaten für <strong>%{domain}</strong> an. Falls dein Konto auf einem anderen Server erstellt wurde, ist eine Anmeldung hier nicht möglich. preamble_html: Melde dich mit deinen Zugangsdaten für <strong>%{domain}</strong> an. Falls dein Konto auf einem anderen Mastodon-Server erstellt wurde, ist eine Anmeldung hier nicht möglich.
title: Bei %{domain} anmelden title: Bei %{domain} anmelden
sign_up: sign_up:
manual_review: Registrierungen für den Server %{domain} werden manuell durch unsere Moderator*innen überprüft. Um uns dabei zu unterstützen, schreibe etwas über dich und sage uns, weshalb du ein Konto auf %{domain} anlegen möchtest. manual_review: Registrierungen für den Server %{domain} werden manuell durch unsere Moderator*innen überprüft. Um uns dabei zu unterstützen, schreibe etwas über dich und sage uns, weshalb du ein Konto auf %{domain} anlegen möchtest.

View File

@@ -563,6 +563,7 @@ eu:
create: Gehitu Moderazio Oharra create: Gehitu Moderazio Oharra
created_msg: Instantziako moderazio oharra ongi sortu da! created_msg: Instantziako moderazio oharra ongi sortu da!
description_html: Ikusi eta idatzi oharrak beste moderatzaileentzat eta zuretzat etorkizunerako description_html: Ikusi eta idatzi oharrak beste moderatzaileentzat eta zuretzat etorkizunerako
title: Moderazio oharrak
private_comment: Iruzkin pribatua private_comment: Iruzkin pribatua
public_comment: Iruzkin publikoa public_comment: Iruzkin publikoa
purge: Ezabatu betiko purge: Ezabatu betiko
@@ -771,6 +772,8 @@ eu:
description_html: Gehienek erabilera baldintzak irakurri eta onartu dituztela baieztatzen badute ere, orokorrean arazoren bat dagoen arte ez dituzte irakurtzen. <strong>Zerbitzariaren arauak begirada batean ikustea errazteko buletadun zerrenda batean bildu.</strong> Saiatu arauak labur eta sinple idazten, baina elementu askotan banatu gabe. description_html: Gehienek erabilera baldintzak irakurri eta onartu dituztela baieztatzen badute ere, orokorrean arazoren bat dagoen arte ez dituzte irakurtzen. <strong>Zerbitzariaren arauak begirada batean ikustea errazteko buletadun zerrenda batean bildu.</strong> Saiatu arauak labur eta sinple idazten, baina elementu askotan banatu gabe.
edit: Editatu araua edit: Editatu araua
empty: Ez da zerbitzariko araurik definitu oraindik. empty: Ez da zerbitzariko araurik definitu oraindik.
move_down: Behera mugitu
move_up: Mugitu gora
title: Zerbitzariaren arauak title: Zerbitzariaren arauak
translation: Itzulpena translation: Itzulpena
translations: Itzulpenak translations: Itzulpenak
@@ -808,6 +811,14 @@ eu:
all: Guztiei all: Guztiei
disabled: Inori ez disabled: Inori ez
users: Saioa hasita duten erabiltzaile lokalei users: Saioa hasita duten erabiltzaile lokalei
feed_access:
modes:
public: Edonork
landing_page:
values:
about: Honi buruz
local_feed: Jario lokala
trends: Joerak
registrations: registrations:
moderation_recommandation: Mesedez, ziurtatu moderazio-talde egokia eta erreaktiboa duzula erregistroak guztiei ireki aurretik! moderation_recommandation: Mesedez, ziurtatu moderazio-talde egokia eta erreaktiboa duzula erregistroak guztiei ireki aurretik!
preamble: Kontrolatu nork sortu dezakeen kontua zerbitzarian. preamble: Kontrolatu nork sortu dezakeen kontua zerbitzarian.
@@ -862,6 +873,7 @@ eu:
original_status: Jatorrizko bidalketa original_status: Jatorrizko bidalketa
quotes: Aipuak quotes: Aipuak
reblogs: Bultzadak reblogs: Bultzadak
replied_to_html: "%{acct_link}(r)i erantzuten"
status_changed: Bidalketa aldatuta status_changed: Bidalketa aldatuta
status_title: "%{name} erabiltzailearen bidalketa" status_title: "%{name} erabiltzailearen bidalketa"
trending: Joera trending: Joera

View File

@@ -1672,6 +1672,7 @@ hu:
disabled_account: A jelenlegi fiókod nem lesz teljesen használható ezután. Viszont elérhető lesz majd az adatexport funkció, valamint a reaktiválás is. disabled_account: A jelenlegi fiókod nem lesz teljesen használható ezután. Viszont elérhető lesz majd az adatexport funkció, valamint a reaktiválás is.
followers: Ez a művelet az összes követődet a jelenlegi fiókról az újra fogja költöztetni followers: Ez a művelet az összes követődet a jelenlegi fiókról az újra fogja költöztetni
only_redirect_html: Az is lehetséges, hogy <a href="%{path}">csak átirányítást raksz a profilodra</a>. only_redirect_html: Az is lehetséges, hogy <a href="%{path}">csak átirányítást raksz a profilodra</a>.
other_data: Semmilyen más adat (beleértve a bejegyzéseket és a követett fiókokat) nem lesz automatikusan áthelyezve
redirect: A jelenlegi fiókod profiljára átirányításról szóló figyelmeztetést rakunk, valamint már nem fogjuk mutatni a keresésekben redirect: A jelenlegi fiókod profiljára átirányításról szóló figyelmeztetést rakunk, valamint már nem fogjuk mutatni a keresésekben
moderation: moderation:
title: Moderáció title: Moderáció
@@ -1928,6 +1929,7 @@ hu:
errors: errors:
in_reply_not_found: Már nem létezik az a bejegyzés, melyre válaszolni szeretnél. in_reply_not_found: Már nem létezik az a bejegyzés, melyre válaszolni szeretnél.
quoted_status_not_found: Már nem létezik az a bejegyzés, amelyből idézni szeretnél. quoted_status_not_found: Már nem létezik az a bejegyzés, amelyből idézni szeretnél.
quoted_user_not_mentioned: Nem idézhet meg nem említett felhasználót egy privát említési bejegyzésben.
over_character_limit: túllépted a maximális %{max} karakteres keretet over_character_limit: túllépted a maximális %{max} karakteres keretet
pin_errors: pin_errors:
direct: A csak a megemlített felhasználók számára látható bejegyzések nem tűzhetők ki direct: A csak a megemlített felhasználók számára látható bejegyzések nem tűzhetők ki

View File

@@ -928,6 +928,7 @@ ru:
no_status_selected: Ничего не изменилось, так как ни один пост не был выделен no_status_selected: Ничего не изменилось, так как ни один пост не был выделен
open: Открыть запись open: Открыть запись
original_status: Оригинальный пост original_status: Оригинальный пост
quotes: Цитаты
reblogs: Продвинули reblogs: Продвинули
replied_to_html: Ответ пользователю %{acct_link} replied_to_html: Ответ пользователю %{acct_link}
status_changed: Пост изменен status_changed: Пост изменен
@@ -935,6 +936,7 @@ ru:
title: Посты пользователя - @%{name} title: Посты пользователя - @%{name}
trending: Популярное trending: Популярное
view_publicly: Открыть по публичной ссылке view_publicly: Открыть по публичной ссылке
view_quoted_post: Просмотр цитируемого сообщения
visibility: Видимость visibility: Видимость
with_media: С файлами with_media: С файлами
strikes: strikes:

View File

@@ -374,7 +374,9 @@ cs:
jurisdiction: Právní příslušnost jurisdiction: Právní příslušnost
min_age: Věková hranice min_age: Věková hranice
user: user:
date_of_birth_1i: Rok
date_of_birth_2i: Měsíc date_of_birth_2i: Měsíc
date_of_birth_3i: Den
role: Role role: Role
time_zone: Časové pásmo time_zone: Časové pásmo
user_role: user_role:

View File

@@ -376,7 +376,9 @@ cy:
jurisdiction: Awdurdodaeth gyfreithiol jurisdiction: Awdurdodaeth gyfreithiol
min_age: Isafswm oedran min_age: Isafswm oedran
user: user:
date_of_birth_1i: Blwyddyn
date_of_birth_2i: Mis date_of_birth_2i: Mis
date_of_birth_3i: Diwrnod
role: Rôl role: Rôl
time_zone: Cylchfa amser time_zone: Cylchfa amser
user_role: user_role:

View File

@@ -353,10 +353,10 @@ de:
indexable: Profilseite in Suchmaschinen einbeziehen indexable: Profilseite in Suchmaschinen einbeziehen
show_application: App anzeigen, über die ich einen Beitrag veröffentlicht habe show_application: App anzeigen, über die ich einen Beitrag veröffentlicht habe
tag: tag:
listable: Erlaube, dass dieser Hashtag in Suchen und Empfehlungen erscheint listable: Dieser Hashtag darf in Suchen und Empfehlungen erscheinen
name: Hashtag name: Hashtag
trendable: Erlaube, dass dieser Hashtag in den Trends erscheint trendable: Dieser Hashtag darf in den Trends erscheinen
usable: Beiträge dürfen diesen Hashtag lokal verwenden usable: Dieser Hashtag darf lokal in Beiträgen verwendet werden
terms_of_service: terms_of_service:
changelog: Was hat sich geändert? changelog: Was hat sich geändert?
effective_date: Datum des Inkrafttretens effective_date: Datum des Inkrafttretens

View File

@@ -88,6 +88,7 @@ hu:
activity_api_enabled: Helyi bejegyzések, aktív felhasználók és új regisztrációk száma heti bontásban activity_api_enabled: Helyi bejegyzések, aktív felhasználók és új regisztrációk száma heti bontásban
app_icon: WEBP, PNG, GIF vagy JPG. Mobileszközökön az alkalmazás alapértelmezett ikonját felülírja egy egyéni ikonnal. app_icon: WEBP, PNG, GIF vagy JPG. Mobileszközökön az alkalmazás alapértelmezett ikonját felülírja egy egyéni ikonnal.
backups_retention_period: A felhasználók archívumokat állíthatnak elő a bejegyzéseikből, hogy később letöltsék azokat. Ha pozitív értékre van állítva, akkor a megadott számú nap után automatikusan törölve lesznek a tárhelyedről. backups_retention_period: A felhasználók archívumokat állíthatnak elő a bejegyzéseikből, hogy később letöltsék azokat. Ha pozitív értékre van állítva, akkor a megadott számú nap után automatikusan törölve lesznek a tárhelyedről.
bootstrap_timeline_accounts: Ezek a fiókok rögzítve lesznek az új felhasználók követési ajánlásai tetején. Add meg a fiókok vesszővel elválasztott listáját.
closed_registrations_message: Akkor jelenik meg, amikor a regisztráció le van zárva closed_registrations_message: Akkor jelenik meg, amikor a regisztráció le van zárva
content_cache_retention_period: Minden más kiszolgálóról származó bejegyzés (megtolásokkal és válaszokkal együtt) törölve lesz a megadott számú nap elteltével, függetlenül a helyi felhasználók ezekkel a bejegyzésekkel történő interakcióitól. Ebben azok a bejegyzések is benne vannak, melyeket a helyi felhasználó könyvjelzőzött vagy kedvencnek jelölt. A különböző kiszolgálók felhasználói közötti privát üzenetek is el fognak veszni visszaállíthatatlanul. Ennek a beállításnak a használata különleges felhasználási esetekre javasolt, mert számos felhasználói elvárás fog eltörni, ha általános céllal használják. content_cache_retention_period: Minden más kiszolgálóról származó bejegyzés (megtolásokkal és válaszokkal együtt) törölve lesz a megadott számú nap elteltével, függetlenül a helyi felhasználók ezekkel a bejegyzésekkel történő interakcióitól. Ebben azok a bejegyzések is benne vannak, melyeket a helyi felhasználó könyvjelzőzött vagy kedvencnek jelölt. A különböző kiszolgálók felhasználói közötti privát üzenetek is el fognak veszni visszaállíthatatlanul. Ennek a beállításnak a használata különleges felhasználási esetekre javasolt, mert számos felhasználói elvárás fog eltörni, ha általános céllal használják.
custom_css: A Mastodon webes verziójában használhatsz egyéni stílusokat. custom_css: A Mastodon webes verziójában használhatsz egyéni stílusokat.
@@ -371,7 +372,9 @@ hu:
jurisdiction: Joghatóság jurisdiction: Joghatóság
min_age: Minimális életkor min_age: Minimális életkor
user: user:
date_of_birth_1i: Év
date_of_birth_2i: Hónap date_of_birth_2i: Hónap
date_of_birth_3i: Nap
role: Szerep role: Szerep
time_zone: Időzóna time_zone: Időzóna
user_role: user_role:

View File

@@ -40,13 +40,15 @@ export function MastodonThemes(): Plugin {
// Get all files mentioned in the themes.yml file. // Get all files mentioned in the themes.yml file.
const themes = await loadThemesFromConfig(projectRoot); const themes = await loadThemesFromConfig(projectRoot);
const allThemes = {
...themes,
default_theme_tokens: 'styles_new/application.scss',
'mastodon-light_theme_tokens': 'styles_new/mastodon-light.scss',
contrast_theme_tokens: 'styles_new/contrast.scss',
};
for (const [themeName, themePath] of Object.entries(themes)) { for (const [themeName, themePath] of Object.entries(allThemes)) {
entrypoints[`themes/${themeName}`] = path.resolve(jsRoot, themePath); entrypoints[`themes/${themeName}`] = path.resolve(jsRoot, themePath);
entrypoints[`themes/${themeName}_theme_tokens`] = path.resolve(
jsRoot,
themePath.replace('styles/', 'styles_new/'),
);
} }
return { return {

View File

@@ -107,9 +107,9 @@ module Mastodon::CLI
IpBlock.severity_no_access.find_each do |ip_block| IpBlock.severity_no_access.find_each do |ip_block|
case options[:format] case options[:format]
when 'nginx' when 'nginx'
say "deny #{ip_block.ip}/#{ip_block.ip.prefix};" say "deny #{ip_block.to_cidr};"
else else
say "#{ip_block.ip}/#{ip_block.ip.prefix}" say ip_block.to_cidr
end end
end end
end end

View File

@@ -123,12 +123,12 @@ module Mastodon::CLI
progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose] progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose]
begin begin
move_previous_to_upgraded move_previous_to_upgraded(previous_path, upgraded_path)
rescue => e rescue => e
progress.log(pastel.red("Error processing #{previous_path}: #{e}")) progress.log(pastel.red("Error processing #{previous_path}: #{e}"))
success = false success = false
remove_directory remove_directory(upgraded_path)
end end
end end

View File

@@ -251,12 +251,12 @@ RSpec.describe Mastodon::CLI::IpBlocks do
it 'exports blocked IPs with "no_access" severity in plain format' do it 'exports blocked IPs with "no_access" severity in plain format' do
expect { subject } expect { subject }
.to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}") .to output_results("#{first_ip_range_block.to_cidr}\n#{second_ip_range_block.to_cidr}")
end end
it 'does not export blocked IPs with different severities' do it 'does not export blocked IPs with different severities' do
expect { subject } expect { subject }
.to_not output_results("#{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}") .to_not output_results(third_ip_range_block.to_cidr)
end end
end end
@@ -265,19 +265,19 @@ RSpec.describe Mastodon::CLI::IpBlocks do
it 'exports blocked IPs with "no_access" severity in plain format' do it 'exports blocked IPs with "no_access" severity in plain format' do
expect { subject } expect { subject }
.to output_results("deny #{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};\ndeny #{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix};") .to output_results("deny #{first_ip_range_block.to_cidr};\ndeny #{second_ip_range_block.to_cidr};")
end end
it 'does not export blocked IPs with different severities' do it 'does not export blocked IPs with different severities' do
expect { subject } expect { subject }
.to_not output_results("deny #{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};") .to_not output_results("deny #{third_ip_range_block.to_cidr};")
end end
end end
context 'when --format option is not provided' do context 'when --format option is not provided' do
it 'exports blocked IPs in plain format by default' do it 'exports blocked IPs in plain format by default' do
expect { subject } expect { subject }
.to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}") .to output_results("#{first_ip_range_block.to_cidr}\n#{second_ip_range_block.to_cidr}")
end end
end end
end end

View File

@@ -73,8 +73,6 @@ RSpec.describe Mastodon::RedisConfiguration do
end end
shared_examples 'sentinel support' do |prefix = nil| shared_examples 'sentinel support' do |prefix = nil|
prefix = prefix ? "#{prefix}_" : ''
context 'when configuring sentinel support' do context 'when configuring sentinel support' do
around do |example| around do |example|
ClimateControl.modify "#{prefix}REDIS_PASSWORD": 'testpass1', "#{prefix}REDIS_HOST": 'redis2.example.com', "#{prefix}REDIS_SENTINELS": '192.168.0.1:3000,192.168.0.2:4000', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do ClimateControl.modify "#{prefix}REDIS_PASSWORD": 'testpass1', "#{prefix}REDIS_HOST": 'redis2.example.com', "#{prefix}REDIS_SENTINELS": '192.168.0.1:3000,192.168.0.2:4000', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
@@ -199,7 +197,7 @@ RSpec.describe Mastodon::RedisConfiguration do
it_behaves_like 'secondary configuration', 'SIDEKIQ' it_behaves_like 'secondary configuration', 'SIDEKIQ'
it_behaves_like 'setting a different driver' it_behaves_like 'setting a different driver'
it_behaves_like 'sentinel support', 'SIDEKIQ' it_behaves_like 'sentinel support', 'SIDEKIQ_'
end end
describe '#cache' do describe '#cache' do
@@ -225,6 +223,6 @@ RSpec.describe Mastodon::RedisConfiguration do
it_behaves_like 'secondary configuration', 'CACHE' it_behaves_like 'secondary configuration', 'CACHE'
it_behaves_like 'setting a different driver' it_behaves_like 'setting a different driver'
it_behaves_like 'sentinel support', 'CACHE' it_behaves_like 'sentinel support', 'CACHE_'
end end
end end

View File

@@ -26,6 +26,22 @@ RSpec.describe IpBlock do
end end
end end
describe '#to_cidr' do
subject { Fabricate.build(:ip_block, ip:).to_cidr }
context 'with an IP and a specified prefix' do
let(:ip) { '192.168.1.0/24' }
it { is_expected.to eq('192.168.1.0/24') }
end
context 'with an IP and a default prefix' do
let(:ip) { '192.168.1.0' }
it { is_expected.to eq('192.168.1.0/32') }
end
end
describe '.blocked?' do describe '.blocked?' do
context 'when the IP is blocked' do context 'when the IP is blocked' do
it 'returns true' do it 'returns true' do

View File

@@ -97,7 +97,7 @@ RSpec.describe 'IP Blocks' do
expect(response.parsed_body) expect(response.parsed_body)
.to include( .to include(
ip: eq("#{ip_block.ip}/#{ip_block.ip.prefix}"), ip: eq(ip_block.to_cidr),
severity: eq(ip_block.severity.to_s) severity: eq(ip_block.severity.to_s)
) )
end end
@@ -216,7 +216,7 @@ RSpec.describe 'IP Blocks' do
expect(response.content_type) expect(response.content_type)
.to start_with('application/json') .to start_with('application/json')
expect(response.parsed_body).to match(hash_including({ expect(response.parsed_body).to match(hash_including({
ip: "#{ip_block.ip}/#{ip_block.ip.prefix}", ip: ip_block.to_cidr,
severity: 'sign_up_requires_approval', severity: 'sign_up_requires_approval',
comment: 'Decreasing severity', comment: 'Decreasing severity',
})) }))

View File

@@ -50,6 +50,21 @@ RSpec.describe 'API V1 Statuses Translations' do
end end
end end
context 'with a public status marked with the same language as the current locale when translation backend cannot do same-language translation' do
let(:status) { Fabricate(:status, account: user.account, text: 'Esto está en español pero está marcado como inglés.', language: 'en') }
it 'returns http forbidden with error message' do
subject
expect(response)
.to have_http_status(403)
expect(response.media_type)
.to eq('application/json')
expect(response.parsed_body)
.to include(error: /not allowed/)
end
end
context 'with a private status' do context 'with a private status' do
let(:status) { Fabricate(:status, visibility: :private, account: user.account, text: 'Hola', language: 'es') } let(:status) { Fabricate(:status, visibility: :private, account: user.account, text: 'Hola', language: 'es') }

View File

@@ -97,39 +97,39 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.28.0, @babel/core@npm:^7.28.4": "@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.28.0, @babel/core@npm:^7.28.5":
version: 7.28.4 version: 7.28.5
resolution: "@babel/core@npm:7.28.4" resolution: "@babel/core@npm:7.28.5"
dependencies: dependencies:
"@babel/code-frame": "npm:^7.27.1" "@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.3" "@babel/generator": "npm:^7.28.5"
"@babel/helper-compilation-targets": "npm:^7.27.2" "@babel/helper-compilation-targets": "npm:^7.27.2"
"@babel/helper-module-transforms": "npm:^7.28.3" "@babel/helper-module-transforms": "npm:^7.28.3"
"@babel/helpers": "npm:^7.28.4" "@babel/helpers": "npm:^7.28.4"
"@babel/parser": "npm:^7.28.4" "@babel/parser": "npm:^7.28.5"
"@babel/template": "npm:^7.27.2" "@babel/template": "npm:^7.27.2"
"@babel/traverse": "npm:^7.28.4" "@babel/traverse": "npm:^7.28.5"
"@babel/types": "npm:^7.28.4" "@babel/types": "npm:^7.28.5"
"@jridgewell/remapping": "npm:^2.3.5" "@jridgewell/remapping": "npm:^2.3.5"
convert-source-map: "npm:^2.0.0" convert-source-map: "npm:^2.0.0"
debug: "npm:^4.1.0" debug: "npm:^4.1.0"
gensync: "npm:^1.0.0-beta.2" gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3" json5: "npm:^2.2.3"
semver: "npm:^6.3.1" semver: "npm:^6.3.1"
checksum: 10c0/ef5a6c3c6bf40d3589b5593f8118cfe2602ce737412629fb6e26d595be2fcbaae0807b43027a5c42ec4fba5b895ff65891f2503b5918c8a3ea3542ab44d4c278 checksum: 10c0/535f82238027621da6bdffbdbe896ebad3558b311d6f8abc680637a9859b96edbf929ab010757055381570b29cf66c4a295b5618318d27a4273c0e2033925e72
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/generator@npm:^7.28.3": "@babel/generator@npm:^7.28.5":
version: 7.28.3 version: 7.28.5
resolution: "@babel/generator@npm:7.28.3" resolution: "@babel/generator@npm:7.28.5"
dependencies: dependencies:
"@babel/parser": "npm:^7.28.3" "@babel/parser": "npm:^7.28.5"
"@babel/types": "npm:^7.28.2" "@babel/types": "npm:^7.28.5"
"@jridgewell/gen-mapping": "npm:^0.3.12" "@jridgewell/gen-mapping": "npm:^0.3.12"
"@jridgewell/trace-mapping": "npm:^0.3.28" "@jridgewell/trace-mapping": "npm:^0.3.28"
jsesc: "npm:^3.0.2" jsesc: "npm:^3.0.2"
checksum: 10c0/0ff58bcf04f8803dcc29479b547b43b9b0b828ec1ee0668e92d79f9e90f388c28589056637c5ff2fd7bcf8d153c990d29c448d449d852bf9d1bc64753ca462bc checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752
languageName: node languageName: node
linkType: hard linkType: hard
@@ -334,7 +334,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4": "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5":
version: 7.28.5 version: 7.28.5
resolution: "@babel/parser@npm:7.28.5" resolution: "@babel/parser@npm:7.28.5"
dependencies: dependencies:
@@ -1194,22 +1194,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4": "@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.5":
version: 7.28.4 version: 7.28.5
resolution: "@babel/traverse@npm:7.28.4" resolution: "@babel/traverse@npm:7.28.5"
dependencies: dependencies:
"@babel/code-frame": "npm:^7.27.1" "@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.3" "@babel/generator": "npm:^7.28.5"
"@babel/helper-globals": "npm:^7.28.0" "@babel/helper-globals": "npm:^7.28.0"
"@babel/parser": "npm:^7.28.4" "@babel/parser": "npm:^7.28.5"
"@babel/template": "npm:^7.27.2" "@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.4" "@babel/types": "npm:^7.28.5"
debug: "npm:^4.3.1" debug: "npm:^4.3.1"
checksum: 10c0/ee678fdd49c9f54a32e07e8455242390d43ce44887cea6567b233fe13907b89240c377e7633478a32c6cf1be0e17c2f7f3b0c59f0666e39c5074cc47b968489c checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4": "@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
version: 7.28.5 version: 7.28.5
resolution: "@babel/types@npm:7.28.5" resolution: "@babel/types@npm:7.28.5"
dependencies: dependencies:
@@ -3317,10 +3317,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@rolldown/pluginutils@npm:1.0.0-beta.43": "@rolldown/pluginutils@npm:1.0.0-beta.47":
version: 1.0.0-beta.43 version: 1.0.0-beta.47
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.43" resolution: "@rolldown/pluginutils@npm:1.0.0-beta.47"
checksum: 10c0/1c17a0b16c277a0fdbab080fd22ef91e37c1f0d710ecfdacb6a080068062eb14ff030d0e9d2ec2325a1d4246dba0c49625755c82c0090f6cbf98d16e80183e02 checksum: 10c0/eb0cfa7334d66f090c47eaac612174936b05f26e789352428cb6e03575b590f355de30d26b42576ea4e613d8887b587119d19b2e4b3a8909ceb232ca1cf746c8
languageName: node languageName: node
linkType: hard linkType: hard
@@ -4815,18 +4815,18 @@ __metadata:
linkType: hard linkType: hard
"@vitejs/plugin-react@npm:^5.0.0": "@vitejs/plugin-react@npm:^5.0.0":
version: 5.1.0 version: 5.1.1
resolution: "@vitejs/plugin-react@npm:5.1.0" resolution: "@vitejs/plugin-react@npm:5.1.1"
dependencies: dependencies:
"@babel/core": "npm:^7.28.4" "@babel/core": "npm:^7.28.5"
"@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1"
"@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1"
"@rolldown/pluginutils": "npm:1.0.0-beta.43" "@rolldown/pluginutils": "npm:1.0.0-beta.47"
"@types/babel__core": "npm:^7.20.5" "@types/babel__core": "npm:^7.20.5"
react-refresh: "npm:^0.18.0" react-refresh: "npm:^0.18.0"
peerDependencies: peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
checksum: 10c0/e192a12e2b854df109eafb1d06c0bc848e8e2b162c686aa6b999b1048658983e72674b2068ccc37562fcce44d32ad92b65f3a4e1897a0cb7859c2ee69cc63eac checksum: 10c0/e590efaea1eabfbb1beb6e8c9fac0742fd299808e3368e63b2825ce24740adb8a28fcb2668b14b7ca1bdb42890cfefe94d02dd358dcbbf8a27ddf377b9a82abf
languageName: node languageName: node
linkType: hard linkType: hard