Skip to content
Commits on Source (10)
# [1.6.0](https://code.castopod.org/adaures/castopod/compare/v1.5.2...v1.6.0) (2023-08-28)
### Bug Fixes
- **home:** update where clause when getting all podcasts to prevent draft
podcasts from showing up
([7a1eea5](https://code.castopod.org/adaures/castopod/commit/7a1eea58d3cbc1982baaec21d87a36e218e1910a))
- **media:** copy and delete temp file when saving instead of moving it for FS
FileManager
([9346e78](https://code.castopod.org/adaures/castopod/commit/9346e787bd2a2c815533092279f96ae1fe0d9aae)),
closes [#338](https://code.castopod.org/adaures/castopod/issues/338)
- **media:** get path using media_path_absolute when saving media file
([754e7a6](https://code.castopod.org/adaures/castopod/commit/754e7a6b4b2c12cf50c1c8b166732dc3255f36fb))
- **media:** init file properties in setAttributes' Model method + set defaults
to pathinfo data
([0775add](https://code.castopod.org/adaures/castopod/commit/0775add67860b94a35b68c01b133ec8ec969f539))
- **premium-podcasts:** show premium flag only when podcast has published
premium episodes
([d10c4fd](https://code.castopod.org/adaures/castopod/commit/d10c4fd7538e6af8a5b0eb232a06522fe8c4bf8e))
- **s3:** add a flag to serve media files by redirecting to a presigned url
instead of default proxy
([11aa358](https://code.castopod.org/adaures/castopod/commit/11aa3586a04c166404954600235634cee77219df))
### Features
- **episode:** add preview link in admin to view and share episode before
publication
([7d21b35](https://code.castopod.org/adaures/castopod/commit/7d21b3509ec5d1aa65420efa038f44bcd235e64f))
## [1.5.2](https://code.castopod.org/adaures/castopod/compare/v1.5.1...v1.5.2) (2023-07-31)
### Bug Fixes
......
......@@ -11,7 +11,7 @@ declare(strict_types=1);
|
| NOTE: this constant is updated upon release with Continuous Integration.
*/
defined('CP_VERSION') || define('CP_VERSION', '1.5.2');
defined('CP_VERSION') || define('CP_VERSION', '1.6.0');
/*
| --------------------------------------------------------------------
......
......@@ -210,6 +210,15 @@ $routes->get('audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioControl
'as' => 'episode-audio',
], );
// episode preview link
$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [
'as' => 'episode-preview',
]);
$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
'as' => 'episode-preview-activity',
]);
// Other pages
$routes->get('/credits', 'CreditsController', [
'as' => 'credits',
......
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Episode;
use App\Models\EpisodeModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class EpisodePreviewController extends BaseController
{
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 1) {
throw PageNotFoundException::forPageNotFound();
}
// find episode by previewUUID
$episode = (new EpisodeModel())->getEpisodeByPreviewId($params[0]);
if (! $episode instanceof Episode) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if ($episode->publication_status === 'published') {
// redirect to episode page
return redirect()->route('episode', [$episode->podcast->handle, $episode->slug]);
}
unset($params[0]);
return $this->{$method}(...$params);
}
public function index(): RedirectResponse | string
{
helper('form');
return view('episode/preview-comments', [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
]);
}
public function activity(): RedirectResponse | string
{
helper('form');
return view('episode/preview-activity', [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
class AddEpisodePreviewId extends BaseMigration
{
public function up(): void
{
$fields = [
'preview_id' => [
'type' => 'BINARY',
'constraint' => 16,
'after' => 'podcast_id',
],
];
$this->forge->addColumn('episodes', $fields);
// set preview_id as unique key
$prefix = $this->db->getPrefix();
$uniquePreviewId = <<<CODE_SAMPLE
ALTER TABLE `{$prefix}episodes`
ADD CONSTRAINT `preview_id` UNIQUE (`preview_id`);
CODE_SAMPLE;
$this->db->query($uniquePreviewId);
}
public function down(): void
{
$fields = ['preview_id'];
$this->forge->dropColumn('episodes', $fields);
}
}
......@@ -14,6 +14,7 @@ use App\Entities\Clip\Soundbite;
use App\Libraries\SimpleRSSElement;
use App\Models\ClipModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
......@@ -21,6 +22,7 @@ use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time;
use Exception;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
......@@ -39,6 +41,8 @@ use SimpleXMLElement;
* @property int $id
* @property int $podcast_id
* @property Podcast $podcast
* @property ?string $preview_id
* @property string $preview_link
* @property string $link
* @property string $guid
* @property string $slug
......@@ -150,6 +154,7 @@ class Episode extends Entity
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
'preview_id' => '?string',
'guid' => 'string',
'slug' => 'string',
'title' => 'string',
......@@ -238,10 +243,7 @@ class Episode extends Entity
(new MediaModel('audio'))->updateMedia($this->getAudio());
} else {
$audio = new Audio([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . pathinfo(
$file->getRandomName(),
PATHINFO_FILENAME
) . '.' . $file->getExtension(),
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $file->getRandomName(),
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
......@@ -512,7 +514,7 @@ class Episode extends Entity
if ($this->getPodcast()->episode_description_footer_html) {
$descriptionHtml .= "<footer>{$this->getPodcast()
->episode_description_footer_html}</footer>";
->episode_description_footer_html}</footer>";
}
return $descriptionHtml;
......@@ -670,4 +672,18 @@ class Episode extends Entity
urlencode((string) $this->attributes['guid']) .
($serviceSlug !== null ? '&_from=' . $serviceSlug : '');
}
public function getPreviewLink(): string
{
if ($this->preview_id === null) {
// generate preview id
if (! $previewUUID = (new EpisodeModel())->setEpisodePreviewId($this->id)) {
throw new Exception('Could not set episode preview id');
}
$this->preview_id = $previewUUID;
}
return url_to('episode-preview', (string) $this->preview_id);
}
}
......@@ -9,6 +9,7 @@ declare(strict_types=1);
*/
use App\Entities\Category;
use App\Entities\Episode;
use App\Entities\Location;
use CodeIgniter\I18n\Time;
use CodeIgniter\View\Table;
......@@ -218,8 +219,8 @@ if (! function_exists('publication_status_banner')) {
}
return <<<HTML
<div class="flex items-center px-12 py-2 border-b bg-stripes-gray border-subtle" role="alert">
<p class="flex items-center text-gray-900">
<div class="flex flex-wrap items-baseline px-4 py-2 border-b md:px-12 bg-stripes-default border-subtle" role="alert">
<p class="flex items-baseline text-gray-900">
<span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
<span class="ml-3 text-sm">{$bannerText}</span>
</p>
......@@ -231,6 +232,58 @@ if (! function_exists('publication_status_banner')) {
// ------------------------------------------------------------------------
if (! function_exists('episode_publication_status_banner')) {
/**
* Publication status banner component for podcasts
*
* Displays the appropriate banner depending on the podcast's publication status.
*/
function episode_publication_status_banner(Episode $episode, string $class = ''): string
{
switch ($episode->publication_status) {
case 'not_published':
$linkRoute = route_to('episode-publish', $episode->podcast_id, $episode->id);
$publishLinkLabel = lang('Episode.publish');
break;
case 'scheduled':
case 'with_podcast':
$linkRoute = route_to('episode-publish_edit', $episode->podcast_id, $episode->id);
$publishLinkLabel = lang('Episode.publish_edit');
break;
default:
$bannerDisclaimer = '';
$linkRoute = '';
$publishLinkLabel = '';
break;
}
$bannerDisclaimer = lang('Episode.publication_status_banner.draft_mode');
$bannerText = lang('Episode.publication_status_banner.text', [
'publication_status' => $episode->publication_status,
'publication_date' => $episode->published_at instanceof Time ? local_datetime(
$episode->published_at
) : null,
], null, false);
$previewLinkLabel = lang('Episode.publication_status_banner.preview');
return <<<HTML
<div class="flex flex-wrap gap-4 items-baseline px-4 md:px-12 py-2 bg-stripes-default border-subtle {$class}" role="alert">
<p class="flex items-baseline text-gray-900">
<span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
<span class="ml-3 text-sm">{$bannerText}</span>
</p>
<div class="flex items-baseline">
<a href="{$episode->preview_link}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$previewLinkLabel}</a>
<span class="mx-1">•</span>
<a href="{$linkRoute}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$publishLinkLabel}</a>
</div>
</div>
HTML;
}
}
// ------------------------------------------------------------------------
if (! function_exists('episode_numbering')) {
/**
* Returns relevant translated episode numbering.
......@@ -360,7 +413,7 @@ if (! function_exists('relative_time')) {
$datetime = $time->format(DateTime::ATOM);
return <<<HTML
<relative-time tense="past" class="{$class}" datetime="{$datetime}">
<relative-time tense="auto" class="{$class}" datetime="{$datetime}">
<time
datetime="{$datetime}"
title="{$time}">{$translatedDate}</time>
......@@ -378,10 +431,10 @@ if (! function_exists('local_datetime')) {
'request'
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
$translatedDate = $time->toLocalizedString($formatter->getPattern());
$datetime = $time->format(DateTime::ISO8601);
$datetime = $time->format(DateTime::ATOM);
return <<<HTML
<relative-time datetime="{$datetime}"
<relative-time datetime="{$datetime}"
prefix=""
threshold="PT0S"
weekday="long"
......
......@@ -82,14 +82,12 @@ if (! function_exists('write_audio_file_tags')) {
// write tags
if ($tagwriter->WriteTags()) {
echo 'Successfully wrote tags<br>';
// Successfully wrote tags
if ($tagwriter->warnings !== []) {
echo 'There were some warnings:<br>' .
implode('<br><br>', $tagwriter->warnings);
log_message('warning', 'There were some warnings:' . PHP_EOL . implode(PHP_EOL, $tagwriter->warnings));
}
} else {
echo 'Failed to write tags!<br>' .
implode('<br><br>', $tagwriter->errors);
log_message('critical', 'Failed to write tags!' . PHP_EOL . implode(PHP_EOL, $tagwriter->errors));
}
}
}
......@@ -25,7 +25,7 @@ return [
one {# publicació}
other {# publicacions}
}',
'links' => 'Links',
'links' => 'Enllaços',
'activity' => 'Activitat',
'episodes' => 'Episodis',
'episodes_title' => 'Episodis de {podcastTitle}',
......
......@@ -9,26 +9,26 @@ declare(strict_types=1);
*/
return [
'title' => "{actorDisplayName}'s comment for {episodeTitle}",
'back_to_comments' => 'Back to comments',
'title' => "{actorDisplayName}s kommentar til {episodeTitle}",
'back_to_comments' => 'Tilbage til kommentarer',
'form' => [
'episode_message_placeholder' => 'Write a comment…',
'reply_to_placeholder' => 'Reply to @{actorUsername}',
'episode_message_placeholder' => 'Skriv en kommentar…',
'reply_to_placeholder' => 'Svar til @{actorUsername}',
'submit' => 'Send',
'submit_reply' => 'Reply',
'submit_reply' => 'Svar',
],
'likes' => '{numberOfLikes, plural,
one {# like}
other {# likes}
one {# kan lide}
other {# kan lide}
}',
'replies' => '{numberOfReplies, plural,
one {# reply}
other {# replies}
one {# svar}
other {# svar}
}',
'like' => 'Like',
'reply' => 'Reply',
'view_replies' => 'View replies ({numberOfReplies})',
'block_actor' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'Delete comment',
'like' => 'Synes godt om',
'reply' => 'Svar',
'view_replies' => 'Se svar ({numberOfReplies})',
'block_actor' => 'Blokér bruger @{actorUsername}',
'block_domain' => 'Blokér domænet @{actorDomain}',
'delete' => 'Slet kommentar',
];
......@@ -9,22 +9,22 @@ declare(strict_types=1);
*/
return [
'yes' => 'Yes',
'no' => 'No',
'cancel' => 'Cancel',
'optional' => 'Optional',
'close' => 'Close',
'home' => 'Home',
'explicit' => 'Explicit',
'powered_by' => 'Powered by {castopod}',
'go_back' => 'Go back',
'yes' => 'Ja',
'no' => 'Nej',
'cancel' => 'Annuller',
'optional' => 'Valg',
'close' => 'Luk',
'home' => 'Hjem',
'explicit' => 'Eksplicit',
'powered_by' => 'Drevet af {castopod}',
'go_back' => 'Gå tilbage',
'play_episode_button' => [
'play' => 'Play',
'playing' => 'Playing',
'play' => 'Afspil',
'playing' => 'Spiller',
],
'read_more' => 'Read more',
'read_less' => 'Read less',
'see_more' => 'See more',
'see_less' => 'See less',
'legal_notice' => 'Legal notice',
'read_more' => 'Læs mere',
'read_less' => 'Læs mindre',
'see_more' => 'Se mere',
'see_less' => 'Se mindre',
'legal_notice' => 'Juridiske oplysninger',
];
......@@ -9,25 +9,25 @@ declare(strict_types=1);
*/
return [
'season' => 'Season {seasonNumber}',
'season' => 'Sæson {seasonNumber}',
'season_abbr' => 'S{seasonNumber}',
'number' => 'Episode {episodeNumber}',
'number_abbr' => 'Ep. {episodeNumber}',
'season_episode' => 'Season {seasonNumber} episode {episodeNumber}',
'season_episode' => 'Sæson {seasonNumber} episode {episodeNumber}',
'season_episode_abbr' => 'S{seasonNumber}:E{episodeNumber}',
'persons' => '{personsCount, plural,
one {# person}
other {# persons}
other {# personer}
}',
'persons_list' => 'Persons',
'back_to_episodes' => 'Back to episodes of {podcast}',
'comments' => 'Comments',
'activity' => 'Activity',
'description' => 'Episode description',
'persons_list' => 'Personer',
'back_to_episodes' => 'Tilbage til episoderne af {podcast}',
'comments' => 'Kommentarer',
'activity' => 'Aktivitet',
'description' => 'Episodebeskrivelse',
'number_of_comments' => '{numberOfComments, plural,
one {# comment}
other {# comments}
one {# kommentar}
other {# kommentarer}
}',
'all_podcast_episodes' => 'All podcast episodes',
'back_to_podcast' => 'Go back to podcast',
'all_podcast_episodes' => 'Alle podcastepisoder',
'back_to_podcast' => 'Tilbage til podcast',
];
......@@ -9,29 +9,29 @@ declare(strict_types=1);
*/
return [
'your_handle' => 'Your handle',
'your_handle_hint' => 'Enter the @username@domain you want to act from.',
'your_handle' => 'Dit handle',
'your_handle_hint' => 'Indtast det @brugernavn@domæne, du ønsker at handle fra.',
'follow' => [
'label' => 'Follow',
'title' => 'Follow {actorDisplayName}',
'subtitle' => 'You are going to follow:',
'accountNotFound' => 'The account could not be found.',
'remoteFollowNotAllowed' => 'Seems like the account server does not allow remote follows…',
'submit' => 'Proceed to follow',
'label' => 'Følg',
'title' => 'Følg {actorDisplayName}',
'subtitle' => 'Du er ved at følge:',
'accountNotFound' => 'Brugeren blev ikke fundet.',
'remoteFollowNotAllowed' => 'Det ser ud til, at kontoserveren ikke tillader eksterne følgere…',
'submit' => 'Fortsæt for at følge',
],
'favourite' => [
'title' => "Favourite {actorDisplayName}'s post",
'subtitle' => 'You are going to favourite:',
'submit' => 'Proceed to favourite',
'title' => "Markér {actorDisplayName}s opslag som favorit",
'subtitle' => 'Du er ved at favoritmarkere:',
'submit' => 'Fortsæt for at favoritmarkere',
],
'reblog' => [
'title' => "Share {actorDisplayName}'s post",
'subtitle' => 'You are going to share:',
'submit' => 'Proceed to share',
'title' => "Del {actorDisplayName}s opslag",
'subtitle' => 'Du er ved at dele:',
'submit' => 'Fortsæt for at dele',
],
'reply' => [
'title' => "Reply to {actorDisplayName}'s post",
'subtitle' => 'You are going to reply to:',
'submit' => 'Proceed to reply',
'title' => "Svar på {actorDisplayName}s opslag",
'subtitle' => 'Du er ved at svare på:',
'submit' => 'Fortsæt for at svare',
],
];
......@@ -9,12 +9,12 @@ declare(strict_types=1);
*/
return [
'all_podcasts' => 'All podcasts',
'sort_by' => 'Sort by',
'all_podcasts' => 'Alle podcasts',
'sort_by' => 'Sortér efter',
'sort_options' => [
'activity' => 'Recent activity',
'created_desc' => 'Newest first',
'created_asc' => 'Oldest first',
'activity' => 'Nylig aktivitet',
'created_desc' => 'Nyeste først',
'created_asc' => 'Ældste først',
],
'no_podcast' => 'No podcast found',
'no_podcast' => 'Ingen podcasts fundet',
];
......@@ -9,9 +9,9 @@ declare(strict_types=1);
*/
return [
'back_to_home' => 'Back to home',
'back_to_home' => 'Tilbage til startsiden',
'map' => [
'title' => 'Map',
'description' => 'Discover podcast episodes on {siteName} that are placed on a map! Travel through the map and listen to episodes that talk about specific locations.',
'title' => 'Kort',
'description' => 'Opdag episoder om {siteName} der er placeret på et kort! Rejs gennem kortet og hør episoder der handler om bestemte steder.',
],
];
......@@ -9,46 +9,46 @@ declare(strict_types=1);
*/
return [
'feed' => 'RSS Podcast feed',
'season' => 'Season {seasonNumber}',
'list_of_episodes_year' => '{year} episodes ({episodeCount})',
'feed' => 'RSS podcast feed',
'season' => 'Sæson {seasonNumber}',
'list_of_episodes_year' => '{year} episoder ({episodeCount})',
'list_of_episodes_season' =>
'Season {seasonNumber} episodes ({episodeCount})',
'no_episode' => 'No episode found!',
'follow' => 'Follow',
'followTitle' => 'Follow {actorDisplayName} on the fediverse!',
'Sæson {seasonNumber} episoder ({episodeCount})',
'no_episode' => 'Ingen afsnit fundet!',
'follow' => 'Følg',
'followTitle' => 'Følg {actorDisplayName} i fediverset!',
'followers' => '{numberOfFollowers, plural,
one {# follower}
other {# followers}
one {# følger}
other {# føgere}
}',
'posts' => '{numberOfPosts, plural,
one {# post}
other {# posts}
one {# indlæg}
other {# indlæg}
}',
'links' => 'Links',
'activity' => 'Activity',
'episodes' => 'Episodes',
'episodes_title' => 'Episodes of {podcastTitle}',
'about' => 'About',
'activity' => 'Aktivitet',
'episodes' => 'Episoder',
'episodes_title' => 'Episoder af {podcastTitle}',
'about' => 'Om',
'stats' => [
'title' => 'Stats',
'title' => 'Statistikker',
'number_of_seasons' => '{0, plural,
one {# season}
other {# seasons}
}',
one {# sæson}
other {# sæsoner}
}',
'number_of_episodes' => '{0, plural,
one {# episode}
other {# episodes}
other {# episoder}
}',
'first_published_at' => 'First episode published on {0, date, medium}',
'first_published_at' => 'Første episode offentliggjort den {0, date, medium}',
],
'sponsor' => 'Sponsor',
'funding_links' => 'Funding links for {podcastTitle}',
'find_on' => 'Find {podcastTitle} on',
'listen_on' => 'Listen on',
'funding_links' => 'Finansieringslinks til {podcastTitle}',
'find_on' => 'Find {podcastTitle} ',
'listen_on' => 'Lyt på',
'persons' => '{personsCount, plural,
one {# person}
other {# persons}
other {# personer}
}',
'persons_list' => 'Persons',
'persons_list' => 'Personer',
];
......@@ -9,32 +9,32 @@ declare(strict_types=1);
*/
return [
'title' => "{actorDisplayName}'s post",
'back_to_actor_posts' => 'Back to {actor} posts',
'actor_shared' => '{actor} shared',
'reply_to' => 'Reply to @{actorUsername}',
'title' => "{actorDisplayName}'s indlæg",
'back_to_actor_posts' => 'Tilbage til {actor} indlæg',
'actor_shared' => '{actor} delt',
'reply_to' => 'Svar @{actorUsername}',
'form' => [
'message_placeholder' => 'Write a message…',
'episode_message_placeholder' => 'Write a message for the episode…',
'message_placeholder' => 'Skriv en besked…',
'episode_message_placeholder' => 'Skriv en besked til episoden…',
'episode_url_placeholder' => 'Episode URL',
'reply_to_placeholder' => 'Reply to @{actorUsername}',
'reply_to_placeholder' => 'Svar @{actorUsername}',
'submit' => 'Send',
'submit_reply' => 'Reply',
'submit_reply' => 'Svar',
],
'favourites' => '{numberOfFavourites, plural,
one {# favourite}
other {# favourites}
one {# kan lide}
other {# kan lide}
}',
'reblogs' => '{numberOfReblogs, plural,
one {# share}
other {# shares}
one {# del}
other {# delinger}
}',
'replies' => '{numberOfReplies, plural,
one {# reply}
other {# replies}
one {# svar}
other {# svar}
}',
'expand' => 'Expand post',
'block_actor' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'Delete post',
'expand' => 'Udvid opslag',
'block_actor' => 'Blokér bruger @{actorUsername}',
'block_domain' => 'Blokér domænet @{actorDomain}',
'delete' => 'Slet indlæg',
];
......@@ -30,4 +30,16 @@ return [
}',
'all_podcast_episodes' => 'All podcast episodes',
'back_to_podcast' => 'Go back to podcast',
'preview' => [
'title' => 'Preview',
'not_published' => 'Not published',
'text' => '{publication_status, select,
published {This episode is not yet published.}
scheduled {This episode is scheduled for publication on {publication_date}.}
with_podcast {This episode will be published at the same time as the podcast.}
other {This episode is not yet published.}
}',
'publish' => 'Publish',
'publish_edit' => 'Edit publication',
],
];
......@@ -25,7 +25,7 @@ return [
one {# publicación}
other {# publicacións}
}',
'links' => 'Links',
'links' => 'Ligazóns',
'activity' => 'Actividade',
'episodes' => 'Episodios',
'episodes_title' => 'Episodios de {podcastTitle}',
......
......@@ -9,7 +9,7 @@ declare(strict_types=1);
*/
return [
'title' => "{actorDisplayName}'s comment for {episodeTitle}",
'title' => "{actorDisplayName} прокоментував {episodeTitle}",
'back_to_comments' => 'Повернутися до коментарів',
'form' => [
'episode_message_placeholder' => 'Написати коментар…',
......@@ -18,12 +18,16 @@ return [
'submit_reply' => 'Відповісти',
],
'likes' => '{numberOfLikes, plural,
one {# like}
other {# likes}
one {# лайк}
few {# подобається}
many {# подобається}
other {# подобається}
}',
'replies' => '{numberOfReplies, plural,
one {# reply}
other {# replies}
one {# коментар}
few {# коментарів}
many {# коментарів}
other {# коментарів}
}',
'like' => 'Вподобайка',
'reply' => 'Відповідь',
......