diff --git a/apps/admin-x-framework/src/test/responses/settings.json b/apps/admin-x-framework/src/test/responses/settings.json index 343b513e8e0..4c9f226a0c5 100644 --- a/apps/admin-x-framework/src/test/responses/settings.json +++ b/apps/admin-x-framework/src/test/responses/settings.json @@ -48,6 +48,34 @@ "key": "twitter", "value": "@ghost" }, + { + "key": "threads", + "value": null + }, + { + "key": "bluesky", + "value": null + }, + { + "key": "mastodon", + "value": null + }, + { + "key": "tiktok", + "value": null + }, + { + "key": "youtube", + "value": null + }, + { + "key": "instagram", + "value": null + }, + { + "key": "linkedin", + "value": null + }, { "key": "navigation", "value": "[{\"label\":\"Home\",\"url\":\"/\"},{\"label\":\"About\",\"url\":\"/about/\"}]" @@ -342,4 +370,4 @@ "group": "site,theme,private,members,portal,newsletter,email,labs,slack,unsplash,views,firstpromoter,editor,comments,analytics,announcement,pintura" } } -} \ No newline at end of file +} diff --git a/apps/admin-x-settings/src/components/settings/general/general-settings.tsx b/apps/admin-x-settings/src/components/settings/general/general-settings.tsx index 70f874af9dd..836a62b46ff 100644 --- a/apps/admin-x-settings/src/components/settings/general/general-settings.tsx +++ b/apps/admin-x-settings/src/components/settings/general/general-settings.tsx @@ -15,7 +15,7 @@ export const searchKeywords = { publicationLanguage: ['general', 'publication language', 'locale'], users: ['general', 'users and permissions', 'roles', 'staff', 'invite people', 'contributors', 'editors', 'authors', 'administrators'], metadata: ['general', 'metadata', 'title', 'description', 'search', 'engine', 'google', 'meta data', 'twitter card', 'structured data', 'rich cards', 'x card', 'social', 'facebook card'], - socialAccounts: ['general', 'social accounts', 'facebook', 'twitter', 'structured data', 'rich cards'], + socialAccounts: ['general', 'social accounts', 'facebook', 'twitter', 'threads', 'bluesky', 'mastodon', 'tiktok', 'youtube', 'instagram', 'linkedin', 'structured data', 'rich cards'], analytics: ['membership', 'analytics', 'tracking', 'privacy', 'membership'] }; diff --git a/apps/admin-x-settings/src/components/settings/general/social-accounts.tsx b/apps/admin-x-settings/src/components/settings/general/social-accounts.tsx index 26bed3f4c37..0aa1e639440 100644 --- a/apps/admin-x-settings/src/components/settings/general/social-accounts.tsx +++ b/apps/admin-x-settings/src/components/settings/general/social-accounts.tsx @@ -1,9 +1,25 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import TopLevelGroup from '../../top-level-group'; import useSettingGroup from '../../../hooks/use-setting-group'; +import {SOCIAL_PLATFORM_CONFIGS, SOCIAL_PLATFORM_KEYS, getSocialValidationError, normalizeSocialInput} from '../../../utils/social-urls'; import {SettingGroupContent, TextField, withErrorBoundary} from '@tryghost/admin-x-design-system'; -import {facebookHandleToUrl, facebookUrlToHandle, twitterHandleToUrl, twitterUrlToHandle, validateFacebookUrl, validateTwitterUrl} from '../../../utils/social-urls'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import type {Setting} from '@tryghost/admin-x-framework/api/settings'; +import type {SocialPlatformKey} from '../../../utils/social-urls'; + +const LEGACY_PLATFORM_KEYS: SocialPlatformKey[] = ['facebook', 'twitter']; +const NEW_PLATFORM_KEYS: SocialPlatformKey[] = SOCIAL_PLATFORM_KEYS.filter( + key => !LEGACY_PLATFORM_KEYS.includes(key) +); + +const getSocialUrls = (localSettings: Setting[] | null) => { + const socialHandles = getSettingValues(localSettings, [...SOCIAL_PLATFORM_KEYS]); + + return Object.fromEntries(SOCIAL_PLATFORM_CONFIGS.map((config, index) => { + const value = socialHandles[index]; + return [config.key, config.toDisplayValue(value)]; + })) as Record; +}; const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => { const { @@ -16,85 +32,52 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => { handleEditingChange } = useSettingGroup(); - const [errors, setErrors] = useState<{ - facebook?: string; - twitter?: string; - }>({}); + const [errors, setErrors] = useState>>({}); + const [urls, setUrls] = useState>(() => getSocialUrls(localSettings)); - const [facebookHandle, twitterHandle] = getSettingValues(localSettings, ['facebook', 'twitter']); + const handles = getSettingValues(localSettings, [...SOCIAL_PLATFORM_KEYS]); + const backendSupportsNewPlatforms = NEW_PLATFORM_KEYS.some(key => localSettings?.some(s => s.key === key)); - const [facebookUrl, setFacebookUrl] = useState(facebookHandle ? facebookHandleToUrl(facebookHandle) : ''); - const [twitterUrl, setTwitterUrl] = useState(twitterHandle ? twitterHandleToUrl(twitterHandle) : ''); + const visiblePlatforms = useMemo(() => { + return backendSupportsNewPlatforms + ? SOCIAL_PLATFORM_CONFIGS + : SOCIAL_PLATFORM_CONFIGS.filter(config => LEGACY_PLATFORM_KEYS.includes(config.key)); + }, [backendSupportsNewPlatforms]); - // Update local state when settings change (e.g., after cancel) + // Depend on stored values, not the localSettings reference (which churns on every updateSetting). useEffect(() => { - setFacebookUrl(facebookHandle ? facebookHandleToUrl(facebookHandle) : ''); - setTwitterUrl(twitterHandle ? twitterHandleToUrl(twitterHandle) : ''); - }, [facebookHandle, twitterHandle]); + setUrls(getSocialUrls(localSettings)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [handles.map(value => value ?? '').join('|')]); - const handleFacebookChange = (value: string) => { - setFacebookUrl(value); - try { - const newUrl = validateFacebookUrl(value); - updateSetting('facebook', facebookUrlToHandle(newUrl)); - if (!isEditing) { - handleEditingChange(true); - } - if (errors.facebook) { - setErrors({...errors, facebook: ''}); - } - } catch (err) { - if (err instanceof Error) { - setErrors({...errors, facebook: err.message}); - } - updateSetting('facebook', null); - } - }; + const handleSocialChange = (key: SocialPlatformKey, value: string) => { + setUrls(current => ({...current, [key]: value})); - const handleTwitterChange = (value: string) => { - setTwitterUrl(value); try { - const newUrl = validateTwitterUrl(value); - updateSetting('twitter', twitterUrlToHandle(newUrl)); + const {storedValue} = normalizeSocialInput(key, value); + updateSetting(key, storedValue); + if (!isEditing) { handleEditingChange(true); } - if (errors.twitter) { - setErrors({...errors, twitter: ''}); - } - } catch (err) { - if (err instanceof Error) { - setErrors({...errors, twitter: err.message}); + + if (errors[key]) { + setErrors(current => ({...current, [key]: ''})); } - updateSetting('twitter', null); + } catch { + setErrors(current => ({...current, [key]: getSocialValidationError(key, value)})); + updateSetting(key, null); } }; const handleSaveClick = () => { - const formErrors: { - facebook?: string; - twitter?: string; - } = {}; - - if (facebookUrl) { - try { - validateFacebookUrl(facebookUrl); - } catch (e) { - if (e instanceof Error) { - formErrors.facebook = e.message; - } + const formErrors = visiblePlatforms.reduce>>((current, config) => { + const error = getSocialValidationError(config.key, urls[config.key]); + if (error) { + current[config.key] = error; } - } - - if (twitterUrl) { - try { - validateTwitterUrl(twitterUrl); - } catch (e) { - if (e instanceof Error) { - formErrors.twitter = e.message; - } - } - } + return current; + }, {}); setErrors(formErrors); @@ -118,22 +101,18 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => { onSave={handleSaveClick} > - handleFacebookChange(e.target.value)} - /> - handleTwitterChange(e.target.value)} - /> + {visiblePlatforms.map(config => ( + handleSocialChange(config.key, event.target.value)} + /> + ))} ); diff --git a/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx b/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx index 693e32d568b..21b6a5ecb3f 100644 --- a/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx @@ -12,10 +12,10 @@ import {ConfirmationModal, Heading, Icon, ImageUpload, LimitModal, Menu, type Me import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {HostLimitError, useLimiter} from '../../../hooks/use-limiter'; import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; +import {SOCIAL_PLATFORM_CONFIGS, getSocialValidationError} from '../../../utils/social-urls/index'; import {type User, canAccessSettings, hasAdminAccess, isAdminUser, isAuthorOrContributor, isEditorUser, isOwnerUser, useDeleteUser, useEditUser, useGetUserBySlug, useMakeOwner} from '@tryghost/admin-x-framework/api/users'; import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images'; import {useGlobalData} from '../../providers/global-data-provider'; -import {validateBlueskyUrl, validateFacebookUrl, validateInstagramUrl, validateLinkedInUrl, validateMastodonUrl, validateThreadsUrl, validateTikTokUrl, validateTwitterUrl, validateYouTubeUrl} from '../../../utils/social-urls/index'; const validators: Record) => string> = { name: ({name}) => { @@ -52,105 +52,10 @@ const validators: Record) => string> = { const valid = !website || (validator.isURL(website) && website.length <= 2000); return valid ? '' : 'Enter a valid URL'; }, - facebook: ({facebook}) => { - try { - validateFacebookUrl(facebook || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - }, - twitter: ({twitter}) => { - try { - validateTwitterUrl(twitter || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - }, - threads: ({threads}) => { - try { - validateThreadsUrl(threads || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - }, - bluesky: ({bluesky}) => { - try { - validateBlueskyUrl(bluesky || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - }, - linkedin: ({linkedin}) => { - try { - validateLinkedInUrl(linkedin || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - }, - instagram: ({instagram}) => { - try { - validateInstagramUrl(instagram || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - }, - youtube: ({youtube}) => { - try { - validateYouTubeUrl(youtube || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - }, - tiktok: ({tiktok}) => { - try { - validateTikTokUrl(tiktok || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - }, - mastodon: ({mastodon}) => { - try { - validateMastodonUrl(mastodon || ''); - return ''; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return ''; - } - } + ...Object.fromEntries(SOCIAL_PLATFORM_CONFIGS.map(config => [ + config.key, + (values: Partial) => getSocialValidationError(config.key, values[config.key] as string | null | undefined) + ])) }; export interface UserDetailProps { diff --git a/apps/admin-x-settings/src/components/settings/general/users/social-links-tab.tsx b/apps/admin-x-settings/src/components/settings/general/users/social-links-tab.tsx index 060789e0106..f588104ad95 100644 --- a/apps/admin-x-settings/src/components/settings/general/users/social-links-tab.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users/social-links-tab.tsx @@ -1,46 +1,16 @@ +import {SOCIAL_PLATFORM_CONFIGS, normalizeSocialInput} from '../../../../utils/social-urls/index'; import {SettingGroup, SettingGroupContent, TextField} from '@tryghost/admin-x-design-system'; import {type UserDetailProps} from '../user-detail-modal'; -import { - blueskyHandleToUrl, - blueskyUrlToHandle, - facebookHandleToUrl, - facebookUrlToHandle, - instagramHandleToUrl, - instagramUrlToHandle, - linkedinHandleToUrl, - linkedinUrlToHandle, - mastodonHandleToUrl, - sanitiseMastodonUrl, - threadsHandleToUrl, - threadsUrlToHandle, - tiktokHandleToUrl, - tiktokUrlToHandle, - twitterHandleToUrl, - twitterUrlToHandle, - validateBlueskyUrl, - validateFacebookUrl, - validateInstagramUrl, - validateLinkedInUrl, - validateMastodonUrl, - validateThreadsUrl, - validateTikTokUrl, - validateTwitterUrl, - validateYouTubeUrl, - youtubeHandleToUrl, - youtubeUrlToHandle -} from '../../../../utils/social-urls/index'; import {useState} from 'react'; +import type {SocialPlatformKey} from '../../../../utils/social-urls/index'; export const DetailsInputs: React.FC = ({errors, clearError, validateField, user, setUserData}) => { - const [facebookUrl, setFacebookUrl] = useState(user.facebook ? facebookHandleToUrl(user.facebook) : ''); - const [twitterUrl, setTwitterUrl] = useState(user.twitter ? twitterHandleToUrl(user.twitter) : ''); - const [threadsUrl, setThreadsUrl] = useState(user.threads ? threadsHandleToUrl(user.threads) : ''); - const [blueskyUrl, setBlueskyUrl] = useState(user.bluesky ? blueskyHandleToUrl(user.bluesky) : ''); - const [linkedinUrl, setLinkedinUrl] = useState(user.linkedin ? linkedinHandleToUrl(user.linkedin) : ''); - const [instagramUrl, setInstagramUrl] = useState(user.instagram ? instagramHandleToUrl(user.instagram) : ''); - const [youtubeUrl, setYoutubeUrl] = useState(user.youtube ? youtubeHandleToUrl(user.youtube) : ''); - const [tiktokUrl, setTiktokUrl] = useState(user.tiktok ? tiktokHandleToUrl(user.tiktok) : ''); - const [mastodonUrl, setMastodonUrl] = useState(user.mastodon ? mastodonHandleToUrl(user.mastodon) : ''); + const [urls, setUrls] = useState>(() => { + return Object.fromEntries(SOCIAL_PLATFORM_CONFIGS.map((config) => { + const value = user[config.key]; + return [config.key, config.toDisplayValue(value)]; + })) as Record; + }); return ( @@ -52,184 +22,32 @@ export const DetailsInputs: React.FC = ({errors, clearError, va placeholder='https://example.com' title="Website" value={user.website || ''} - // onBlur={(e) => { - // validateField('url', e.target.value); - // }} - onChange={(e) => { - setUserData({...user, website: e.target.value}); - }} - onKeyDown={() => clearError('url')} /> - { - if (validateField('twitter', e.target.value)) { - const url = validateTwitterUrl(e.target.value); - setTwitterUrl(url); - setUserData({...user, twitter: twitterUrlToHandle(url)}); - } - }} - onChange={(e) => { - setTwitterUrl(e.target.value); - }} - onKeyDown={() => clearError('twitter')} /> - { - if (validateField('facebook', e.target.value)) { - const url = validateFacebookUrl(e.target.value); - setFacebookUrl(url); - setUserData({...user, facebook: facebookUrlToHandle(url)}); - } - }} - onChange={(e) => { - setFacebookUrl(e.target.value); - }} - onKeyDown={() => clearError('facebook')} /> - { - if (validateField('linkedin', e.target.value)) { - const url = validateLinkedInUrl(e.target.value); - setLinkedinUrl(url); - setUserData({...user, linkedin: linkedinUrlToHandle(url)}); - } - }} - onChange={(e) => { - setLinkedinUrl(e.target.value); - }} - onKeyDown={() => clearError('linkedin')} /> - { - if (validateField('bluesky', e.target.value)) { - const url = validateBlueskyUrl(e.target.value); - setBlueskyUrl(url); - setUserData({...user, bluesky: blueskyUrlToHandle(url)}); - } - }} - onChange={(e) => { - setBlueskyUrl(e.target.value); - }} - onKeyDown={() => clearError('bluesky')} /> - { - if (validateField('threads', e.target.value)) { - const url = validateThreadsUrl(e.target.value); - setThreadsUrl(url); - setUserData({...user, threads: threadsUrlToHandle(url)}); - } - }} - onChange={(e) => { - setThreadsUrl(e.target.value); - }} - onKeyDown={() => clearError('threads')} /> - { - if (validateField('mastodon', e.target.value)) { - const url = validateMastodonUrl(e.target.value); - setMastodonUrl(url); - setUserData({...user, mastodon: sanitiseMastodonUrl(url)}); - } - }} - onChange={(e) => { - setMastodonUrl(e.target.value); - }} - onKeyDown={() => clearError('mastodon')} /> - { - if (validateField('tiktok', e.target.value)) { - const url = validateTikTokUrl(e.target.value); - setTiktokUrl(url); - setUserData({...user, tiktok: tiktokUrlToHandle(url)}); - } - }} - onChange={(e) => { - setTiktokUrl(e.target.value); - }} - onKeyDown={() => clearError('tiktok')} /> - { - if (validateField('youtube', e.target.value)) { - const url = validateYouTubeUrl(e.target.value); - setYoutubeUrl(url); - setUserData({...user, youtube: youtubeUrlToHandle(url)}); - } - }} - onChange={(e) => { - setYoutubeUrl(e.target.value); - }} - onKeyDown={() => clearError('youtube')} /> - { - if (validateField('instagram', e.target.value)) { - const url = validateInstagramUrl(e.target.value); - setInstagramUrl(url); - setUserData({...user, instagram: instagramUrlToHandle(url)}); - } - }} - onChange={(e) => { - setInstagramUrl(e.target.value); - }} - onKeyDown={() => clearError('instagram')} /> + onChange={(event) => { + setUserData({...user, website: event.target.value}); + }} + onKeyDown={() => clearError('website')} /> + {SOCIAL_PLATFORM_CONFIGS.map(config => ( + { + if (validateField(config.key, event.target.value)) { + const {displayValue, storedValue} = normalizeSocialInput(config.key, event.target.value); + setUrls(current => ({...current, [config.key]: displayValue})); + setUserData({...user, [config.key]: storedValue}); + } + }} + onChange={(event) => { + setUrls(current => ({...current, [config.key]: event.target.value})); + }} + onKeyDown={() => clearError(config.key)} /> + ))} ); }; diff --git a/apps/admin-x-settings/src/utils/social-urls/index.ts b/apps/admin-x-settings/src/utils/social-urls/index.ts index c90d3297b21..690c2fa87cf 100644 --- a/apps/admin-x-settings/src/utils/social-urls/index.ts +++ b/apps/admin-x-settings/src/utils/social-urls/index.ts @@ -1,9 +1,10 @@ export * from './facebook'; export * from './twitter'; export * from './threads'; -export * from './bluesky'; +export * from './bluesky'; export * from './linkedin'; export * from './instagram'; export * from './youtube'; export * from './tiktok'; export * from './mastodon'; +export * from './platform-config'; diff --git a/apps/admin-x-settings/src/utils/social-urls/platform-config.ts b/apps/admin-x-settings/src/utils/social-urls/platform-config.ts new file mode 100644 index 00000000000..7f97808de88 --- /dev/null +++ b/apps/admin-x-settings/src/utils/social-urls/platform-config.ts @@ -0,0 +1,159 @@ +import {blueskyHandleToUrl, blueskyUrlToHandle, validateBlueskyUrl} from './bluesky'; +import {facebookHandleToUrl, facebookUrlToHandle, validateFacebookUrl} from './facebook'; +import {instagramHandleToUrl, instagramUrlToHandle, validateInstagramUrl} from './instagram'; +import {linkedinHandleToUrl, linkedinUrlToHandle, validateLinkedInUrl} from './linkedin'; +import {mastodonHandleToUrl, sanitiseMastodonUrl, validateMastodonUrl} from './mastodon'; +import {threadsHandleToUrl, threadsUrlToHandle, validateThreadsUrl} from './threads'; +import {tiktokHandleToUrl, tiktokUrlToHandle, validateTikTokUrl} from './tiktok'; +import {twitterHandleToUrl, twitterUrlToHandle, validateTwitterUrl} from './twitter'; +import {validateYouTubeUrl, youtubeHandleToUrl, youtubeUrlToHandle} from './youtube'; + +export const SOCIAL_PLATFORM_KEYS = [ + 'twitter', + 'facebook', + 'linkedin', + 'bluesky', + 'threads', + 'mastodon', + 'tiktok', + 'youtube', + 'instagram' +] as const; + +export type SocialPlatformKey = typeof SOCIAL_PLATFORM_KEYS[number]; + +export type SocialPlatformConfig = { + key: SocialPlatformKey; + staffTitle: string; + publicationTitle: string; + placeholder: string; + testId: string; + validate: (value: string) => string; + toDisplayValue: (value: string | null | undefined) => string; + toStoredValue: (value: string) => string | null; +}; + +const formatDisplayValue = (value: string | null | undefined, formatter: (handle: string) => string) => { + return value ? formatter(value) : ''; +}; + +export const SOCIAL_PLATFORM_CONFIGS: SocialPlatformConfig[] = [ + { + key: 'twitter', + staffTitle: 'X', + publicationTitle: 'X', + placeholder: 'https://x.com/username', + testId: 'x-input', + validate: validateTwitterUrl, + toDisplayValue: value => formatDisplayValue(value, twitterHandleToUrl), + toStoredValue: value => twitterUrlToHandle(value) + }, + { + key: 'facebook', + staffTitle: 'Facebook', + publicationTitle: 'Facebook', + placeholder: 'https://www.facebook.com/username', + testId: 'facebook-input', + validate: validateFacebookUrl, + toDisplayValue: value => formatDisplayValue(value, facebookHandleToUrl), + toStoredValue: value => facebookUrlToHandle(value) + }, + { + key: 'linkedin', + staffTitle: 'LinkedIn', + publicationTitle: 'LinkedIn', + placeholder: 'https://www.linkedin.com/in/username', + testId: 'linkedin-input', + validate: validateLinkedInUrl, + toDisplayValue: value => formatDisplayValue(value, linkedinHandleToUrl), + toStoredValue: value => linkedinUrlToHandle(value) + }, + { + key: 'bluesky', + staffTitle: 'Bluesky', + publicationTitle: 'Bluesky', + placeholder: 'https://bsky.app/profile/username', + testId: 'bluesky-input', + validate: validateBlueskyUrl, + toDisplayValue: value => formatDisplayValue(value, blueskyHandleToUrl), + toStoredValue: value => blueskyUrlToHandle(value) + }, + { + key: 'threads', + staffTitle: 'Threads', + publicationTitle: 'Threads', + placeholder: 'https://threads.net/@username', + testId: 'threads-input', + validate: validateThreadsUrl, + toDisplayValue: value => formatDisplayValue(value, threadsHandleToUrl), + toStoredValue: value => threadsUrlToHandle(value) + }, + { + key: 'mastodon', + staffTitle: 'Mastodon', + publicationTitle: 'Mastodon', + placeholder: 'https://mastodon.social/@username', + testId: 'mastodon-input', + validate: validateMastodonUrl, + toDisplayValue: value => formatDisplayValue(value, mastodonHandleToUrl), + toStoredValue: value => (value ? sanitiseMastodonUrl(value) : null) + }, + { + key: 'tiktok', + staffTitle: 'TikTok', + publicationTitle: 'TikTok', + placeholder: 'https://www.tiktok.com/@username', + testId: 'tiktok-input', + validate: validateTikTokUrl, + toDisplayValue: value => formatDisplayValue(value, tiktokHandleToUrl), + toStoredValue: value => tiktokUrlToHandle(value) + }, + { + key: 'youtube', + staffTitle: 'YouTube', + publicationTitle: 'YouTube', + placeholder: 'https://www.youtube.com/@channel', + testId: 'youtube-input', + validate: validateYouTubeUrl, + toDisplayValue: value => formatDisplayValue(value, youtubeHandleToUrl), + toStoredValue: value => youtubeUrlToHandle(value) + }, + { + key: 'instagram', + staffTitle: 'Instagram', + publicationTitle: 'Instagram', + placeholder: 'https://www.instagram.com/username', + testId: 'instagram-input', + validate: validateInstagramUrl, + toDisplayValue: value => formatDisplayValue(value, instagramHandleToUrl), + toStoredValue: value => instagramUrlToHandle(value) + } +]; + +export const SOCIAL_PLATFORM_CONFIG_BY_KEY = Object.fromEntries( + SOCIAL_PLATFORM_CONFIGS.map(config => [config.key, config]) +) as Record; + +export const normalizeSocialInput = (key: SocialPlatformKey, value: string) => { + const config = SOCIAL_PLATFORM_CONFIG_BY_KEY[key]; + const normalizedUrl = config.validate(value); + const storedValue = normalizedUrl ? config.toStoredValue(normalizedUrl) : null; + + return { + displayValue: config.toDisplayValue(storedValue), + storedValue + }; +}; + +export const getSocialValidationError = (key: SocialPlatformKey, value: string | null | undefined) => { + try { + SOCIAL_PLATFORM_CONFIG_BY_KEY[key].validate(value || ''); + return ''; + } catch (error) { + if (error instanceof Error) { + return error.message; + } + + return ''; + } +}; diff --git a/apps/admin-x-settings/test/acceptance/general/social-accounts.test.ts b/apps/admin-x-settings/test/acceptance/general/social-accounts.test.ts index a571fda1348..a3b3499b3ea 100644 --- a/apps/admin-x-settings/test/acceptance/general/social-accounts.test.ts +++ b/apps/admin-x-settings/test/acceptance/general/social-accounts.test.ts @@ -1,39 +1,123 @@ import {expect, test} from '@playwright/test'; -import {globalDataRequests, mockApi, testUrlValidation, updatedSettingsResponse} from '@tryghost/admin-x-framework/test/acceptance'; +import {globalDataRequests, mockApi, responseFixtures, testUrlValidation, updatedSettingsResponse} from '@tryghost/admin-x-framework/test/acceptance'; + +const NEW_PLATFORM_KEYS = ['threads', 'bluesky', 'mastodon', 'tiktok', 'youtube', 'instagram', 'linkedin']; + +const editedSocialSettings = [ + {key: 'facebook', value: 'fb', label: 'Facebook', displayValue: 'https://www.facebook.com/fb'}, + {key: 'twitter', value: '@tw', label: 'X', displayValue: 'https://x.com/tw'}, + {key: 'linkedin', value: 'ghost-team', label: 'LinkedIn', displayValue: 'https://www.linkedin.com/in/ghost-team'}, + {key: 'bluesky', value: 'ghost.bsky.social', label: 'Bluesky', displayValue: 'https://bsky.app/profile/ghost.bsky.social'}, + {key: 'threads', value: '@ghostteam', label: 'Threads', displayValue: 'https://www.threads.net/@ghostteam'}, + {key: 'mastodon', value: 'mastodon.social/@ghost', label: 'Mastodon', displayValue: 'https://mastodon.social/@ghost'}, + {key: 'tiktok', value: '@ghostteam', label: 'TikTok', displayValue: 'https://www.tiktok.com/@ghostteam'}, + {key: 'youtube', value: '@ghostteam', label: 'YouTube', displayValue: 'https://www.youtube.com/@ghostteam'}, + {key: 'instagram', value: 'ghostteam', label: 'Instagram', displayValue: 'https://www.instagram.com/ghostteam'} +] as const; + +const sortSettings = (settings: T[]) => { + return [...settings].sort((left, right) => left.key.localeCompare(right.key)); +}; test.describe('Social account settings', async () => { - test('Supports editing social URLs', async ({page}) => { + test('Supports editing all publication social URLs', async ({page}) => { const {lastApiRequests} = await mockApi({page, requests: { ...globalDataRequests, - editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([ - {key: 'facebook', value: 'fb'}, - {key: 'twitter', value: '@tw'} - ])} + editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse(editedSocialSettings.map(({key, value}) => ({key, value})))} }}); await page.goto('/'); const section = page.getByTestId('social-accounts'); - // Check initial values in input fields - await expect(section.getByLabel(`URL of your publication's Facebook Page`)).toHaveValue('https://www.facebook.com/ghost'); - await expect(section.getByLabel('URL of your X profile')).toHaveValue('https://x.com/ghost'); + await expect(section.getByLabel('Facebook')).toHaveValue('https://www.facebook.com/ghost'); + await expect(section.getByLabel('X')).toHaveValue('https://x.com/ghost'); - await section.getByLabel(`URL of your publication's Facebook Page`).fill('https://www.facebook.com/fb'); - await section.getByLabel('URL of your X profile').fill('https://x.com/tw'); + for (const field of editedSocialSettings) { + await section.getByLabel(field.label).fill(field.displayValue); + } await section.getByRole('button', {name: 'Save'}).click(); - // Check updated values in input fields - await expect(section.getByLabel(`URL of your publication's Facebook Page`)).toHaveValue('https://www.facebook.com/fb'); - await expect(section.getByLabel('URL of your X profile')).toHaveValue('https://x.com/tw'); + for (const field of editedSocialSettings) { + await expect(section.getByLabel(field.label)).toHaveValue(field.displayValue); + } + + expect(sortSettings((lastApiRequests.editSettings?.body as {settings: Array<{key: string; value: string}>} | undefined)?.settings ?? [])).toEqual( + sortSettings(editedSocialSettings.map(({key, value}) => ({key, value}))) + ); + }); + + test('Hides the new platform fields when the backend has not deployed the migration yet', async ({page}) => { + // Simulates an admin running ahead of its backend: the settings response + // is missing the keys added by the publication-social-account-settings + // migration. The UI should fall back to facebook + twitter only so the + // user can't enter values that would be silently dropped by a backend + // that doesn't list them in EDITABLE_SETTINGS. + const settingsWithoutNewKeys = { + ...responseFixtures.settings, + settings: responseFixtures.settings.settings.filter(s => !NEW_PLATFORM_KEYS.includes(s.key)) + }; + + await mockApi({page, requests: { + ...globalDataRequests, + browseSettings: {method: 'GET', path: /^\/settings\/\?group=/, response: settingsWithoutNewKeys} + }}); + + await page.goto('/'); + + const section = page.getByTestId('social-accounts'); + + await expect(section.getByLabel('Facebook')).toBeVisible(); + await expect(section.getByLabel('X')).toBeVisible(); + + for (const label of ['LinkedIn', 'Bluesky', 'Threads', 'Mastodon', 'TikTok', 'YouTube', 'Instagram']) { + await expect(section.getByLabel(label)).toHaveCount(0); + } + }); + + test('Shows all new platform fields when any new key is present, even if linkedin is missing', async ({page}) => { + // Defends against the canary being a single hardcoded key. The migration adds + // all 7 keys atomically today, but the UI gate should treat the presence of + // any of them as sufficient — so a future migration that drops/renames just + // one key doesn't silently hide the entire panel. + const settingsWithoutLinkedin = { + ...responseFixtures.settings, + settings: responseFixtures.settings.settings.filter(s => s.key !== 'linkedin') + }; + + await mockApi({page, requests: { + ...globalDataRequests, + browseSettings: {method: 'GET', path: /^\/settings\/\?group=/, response: settingsWithoutLinkedin} + }}); + + await page.goto('/'); + + const section = page.getByTestId('social-accounts'); + + for (const label of ['Facebook', 'X', 'LinkedIn', 'Bluesky', 'Threads', 'Mastodon', 'TikTok', 'YouTube', 'Instagram']) { + await expect(section.getByLabel(label)).toBeVisible(); + } + }); + + test('Restores values on cancel', async ({page}) => { + await mockApi({page, requests: { + ...globalDataRequests + }}); + + await page.goto('/'); + + const section = page.getByTestId('social-accounts'); + const linkedinInput = section.getByLabel('LinkedIn'); + const instagramInput = section.getByLabel('Instagram'); + + await linkedinInput.fill('https://www.linkedin.com/in/ghost-team'); + await instagramInput.fill('https://www.instagram.com/ghostteam'); - expect(lastApiRequests.editSettings?.body).toEqual({ - settings: [ - {key: 'facebook', value: 'fb'}, - {key: 'twitter', value: '@tw'} - ] - }); + await section.getByRole('button', {name: 'Cancel'}).click(); + + await expect(linkedinInput).toHaveValue(''); + await expect(instagramInput).toHaveValue(''); }); test('Formats and validates the URLs', async ({page}) => { @@ -45,8 +129,7 @@ test.describe('Social account settings', async () => { const section = page.getByTestId('social-accounts'); - // Wait for the inputs to be visible - const facebookInput = section.getByLabel(`URL of your publication's Facebook Page`); + const facebookInput = section.getByLabel('Facebook'); await expect(facebookInput).toBeVisible(); await testUrlValidation( @@ -98,8 +181,6 @@ test.describe('Social account settings', async () => { 'The URL must be in a format like https://www.facebook.com/yourPage' ); - // Reset to a valid URL so the setting is non-null before the next invalid test, - // otherwise the useEffect that clears the field won't fire (setting is already null) await testUrlValidation( facebookInput, 'facebook.com/valid', @@ -113,7 +194,7 @@ test.describe('Social account settings', async () => { 'The URL must be in a format like https://www.facebook.com/yourPage' ); - const twitterInput = section.getByLabel('URL of your X profile'); + const twitterInput = section.getByLabel('X'); await testUrlValidation( twitterInput, @@ -140,7 +221,6 @@ test.describe('Social account settings', async () => { 'The URL must be in a format like https://x.com/yourUsername' ); - // Reset to a valid URL so the setting is non-null before the next invalid test await testUrlValidation( twitterInput, 'testuser', @@ -153,5 +233,54 @@ test.describe('Social account settings', async () => { '', 'Your Username is not a valid Twitter Username' ); + + await testUrlValidation( + section.getByLabel('LinkedIn'), + 'ghost-team', + 'https://www.linkedin.com/in/ghost-team' + ); + + await testUrlValidation( + section.getByLabel('LinkedIn'), + 'https://github.com/ghost', + '', + 'The URL must be in a format like https://www.linkedin.com/in/yourUsername' + ); + + await testUrlValidation( + section.getByLabel('Bluesky'), + 'ghost.bsky.social', + 'https://bsky.app/profile/ghost.bsky.social' + ); + + await testUrlValidation( + section.getByLabel('Threads'), + '@ghostteam', + 'https://www.threads.net/@ghostteam' + ); + + await testUrlValidation( + section.getByLabel('Mastodon'), + '@ghost@mastodon.social', + 'https://mastodon.social/@ghost' + ); + + await testUrlValidation( + section.getByLabel('TikTok'), + 'ghostteam', + 'https://www.tiktok.com/@ghostteam' + ); + + await testUrlValidation( + section.getByLabel('YouTube'), + '@ghostteam', + 'https://www.youtube.com/@ghostteam' + ); + + await testUrlValidation( + section.getByLabel('Instagram'), + 'ghostteam', + 'https://www.instagram.com/ghostteam' + ); }); }); diff --git a/apps/admin-x-settings/test/unit/utils/platform-config.test.ts b/apps/admin-x-settings/test/unit/utils/platform-config.test.ts new file mode 100644 index 00000000000..8a62ad3b442 --- /dev/null +++ b/apps/admin-x-settings/test/unit/utils/platform-config.test.ts @@ -0,0 +1,39 @@ +import * as assert from 'assert/strict'; +import {SOCIAL_PLATFORM_CONFIG_BY_KEY, normalizeSocialInput} from '../../../src/utils/social-urls/index'; + +describe('normalizeSocialInput', () => { + it('returns empty display and null stored for empty input', () => { + const result = normalizeSocialInput('linkedin', ''); + assert.equal(result.displayValue, ''); + assert.equal(result.storedValue, null); + }); + + it('produces a displayValue that round-trips through storage', () => { + // The post-blur display must match what the field will show on reload, + // which is toDisplayValue(storedValue). Otherwise the user sees one + // value after blur and a different value after refresh. + const cases: Array<{key: Parameters[0]; input: string}> = [ + {key: 'linkedin', input: 'https://uk.linkedin.com/in/ghost-team'}, + {key: 'linkedin', input: 'linkedin.com/in/ghost-team'}, + {key: 'twitter', input: 'https://x.com/@ghost'}, + {key: 'facebook', input: 'facebook.com/ghost'}, + {key: 'mastodon', input: 'https://mastodon.social/@ghost'}, + {key: 'youtube', input: 'http://youtube.com/@ghost'} + ]; + + for (const {key, input} of cases) { + const {displayValue, storedValue} = normalizeSocialInput(key, input); + const expected = SOCIAL_PLATFORM_CONFIG_BY_KEY[key].toDisplayValue(storedValue); + assert.equal(displayValue, expected, `displayValue should round-trip for ${key} input "${input}"`); + } + }); + + it('strips the regional subdomain from LinkedIn display because it is not in storage', () => { + // `uk.linkedin.com` is a valid input, but the stored handle drops the + // regional prefix. The displayed value after blur should reflect what + // will be displayed on reload — `www.linkedin.com`, not `uk.`. + const {displayValue, storedValue} = normalizeSocialInput('linkedin', 'https://uk.linkedin.com/in/ghost-team'); + assert.equal(storedValue, 'ghost-team'); + assert.equal(displayValue, 'https://www.linkedin.com/in/ghost-team'); + }); +}); diff --git a/apps/portal/package.json b/apps/portal/package.json index a817618c5c5..f92b5f1a758 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/portal", - "version": "2.68.28", + "version": "2.68.29", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", diff --git a/apps/portal/src/components/pages/AccountHomePage/components/paid-account-actions.js b/apps/portal/src/components/pages/AccountHomePage/components/paid-account-actions.js index 27d109141a1..61feb63e36a 100644 --- a/apps/portal/src/components/pages/AccountHomePage/components/paid-account-actions.js +++ b/apps/portal/src/components/pages/AccountHomePage/components/paid-account-actions.js @@ -145,14 +145,11 @@ const PaidAccountActions = () => { return null; }; - const BillingSection = ({defaultCardLast4, isComplimentary}) => { + const BillingSection = ({defaultCardLast4}) => { const {action} = useContext(AppContext); const label = action === 'manageBilling:running' ? ( ) : t('Update'); - if (isComplimentary) { - return null; - } return (
@@ -173,6 +170,7 @@ const PaidAccountActions = () => { const subscription = getMemberSubscription({member}); const isComplimentary = isComplimentaryMember({member}); + const isGift = isGiftMember({member}); const isPaid = isPaidMember({member}); if (subscription || isComplimentary) { const { @@ -203,7 +201,7 @@ const PaidAccountActions = () => {
- + {!isComplimentary && !isGift && } ); } diff --git a/apps/portal/src/components/pages/gift-page.js b/apps/portal/src/components/pages/gift-page.js index d33f6032b0f..23bc3c45b72 100644 --- a/apps/portal/src/components/pages/gift-page.js +++ b/apps/portal/src/components/pages/gift-page.js @@ -1,9 +1,10 @@ -import {useContext, useEffect, useRef, useState} from 'react'; +import {useContext, useLayoutEffect, useRef, useState} from 'react'; import AppContext from '../../app-context'; import CloseButton from '../common/close-button'; import ActionButton from '../common/action-button'; import LoadingPage from './loading-page'; import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg'; +import giftCardOrbUrl from '../../images/gift-card-orb.svg'; import {getAvailableProducts, getCurrencySymbol, formatNumber, getStripeAmount, isCookiesDisabled, getActiveInterval} from '../../utils/helpers'; import useCardTilt from '../../utils/use-card-tilt'; @@ -15,9 +16,20 @@ export const GiftPageStyles = ` .gh-portal-popup-container.full-size.giftSuccess, .gh-portal-popup-container.full-size.giftRedemption { padding: 0; - /* The default opacity/translate popup-in animation feels heavy on the - full-bleed 50/50 layout — let the page render in place instead. */ - animation: none; +} + +/* Close icon sits over the brand-coloured right panel, so override the + default grey from Frame.styles.js to a translucent white. */ +.gh-portal-popup-container.full-size.gift .gh-portal-closeicon, +.gh-portal-popup-container.full-size.giftSuccess .gh-portal-closeicon, +.gh-portal-popup-container.full-size.giftRedemption .gh-portal-closeicon { + color: rgba(255, 255, 255, 0.7); +} + +.gh-portal-popup-container.full-size.gift .gh-portal-closeicon:hover, +.gh-portal-popup-container.full-size.giftSuccess .gh-portal-closeicon:hover, +.gh-portal-popup-container.full-size.giftRedemption .gh-portal-closeicon:hover { + color: rgba(255, 255, 255, 0.95); } .gh-portal-content.gift, @@ -42,7 +54,15 @@ export const GiftPageStyles = ` justify-content: center; background: var(--white); padding: 64px 48px 128px; - overflow: hidden; +} + +/* On the selection page only, the inner content's vertical position is + locked once on first paint by useLayoutEffect (so tier switches push the + CTA down rather than re-centering). flex-start lets that JS-applied + margin-top do the actual centering math. Other gift pages keep the + default flex centering. */ +.gh-portal-content.gift .gh-portal-gift-checkout-left { + align-items: flex-start; } .gh-portal-gift-checkout-bg { @@ -59,7 +79,7 @@ export const GiftPageStyles = ` } .gh-portal-gift-checkout-header { - margin-bottom: 8px; + margin-bottom: 16px; } .gh-portal-gift-checkout-header .gh-portal-main-title { @@ -97,32 +117,42 @@ export const GiftPageStyles = ` gap: 8px; } -.gh-portal-gift-checkout-tier { - display: flex; - align-items: center; - gap: 14px; - width: 100%; +/* Each tier renders as an accordion item: the radio button row sits at top, + and the benefits accordion expands below when selected. The border, radius + and selected-state styling live on the wrapper so the benefits feel like + they belong to the same card. */ +.gh-portal-gift-checkout-tier-item { background: var(--white); border: 1px solid var(--grey11); border-radius: 10px; - padding: 16px 20px; - cursor: pointer; - text-align: start; + overflow: hidden; transition: border-color 0.2s ease, background-color 0.2s ease; - font: inherit; - color: inherit; } -.gh-portal-gift-checkout-tier:hover { +.gh-portal-gift-checkout-tier-item:hover { border-color: var(--grey9); } -.gh-portal-gift-checkout-tier.selected { +.gh-portal-gift-checkout-tier-item.selected { border-color: var(--brandcolor); background: color-mix(in srgb, var(--brandcolor) 6%, var(--white)); box-shadow: 0 0 0 1px var(--brandcolor) inset; } +.gh-portal-gift-checkout-tier { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + background: transparent; + border: none; + padding: 16px 20px; + cursor: pointer; + text-align: start; + font: inherit; + color: inherit; +} + .gh-portal-gift-checkout-tier-radio { flex-shrink: 0; width: 18px; @@ -133,12 +163,12 @@ export const GiftPageStyles = ` position: relative; } -.gh-portal-gift-checkout-tier.selected .gh-portal-gift-checkout-tier-radio { +.gh-portal-gift-checkout-tier-item.selected .gh-portal-gift-checkout-tier-radio { border-color: var(--brandcolor); background: var(--brandcolor); } -.gh-portal-gift-checkout-tier.selected .gh-portal-gift-checkout-tier-radio::after { +.gh-portal-gift-checkout-tier-item.selected .gh-portal-gift-checkout-tier-radio::after { content: ''; position: absolute; top: 50%; @@ -163,11 +193,40 @@ export const GiftPageStyles = ` color: var(--grey0); } +/* Accordion wrapper inside each tier — collapses and expands its benefit + list using the grid-template-rows trick, matching the 0.3s ease used by + the right-side details toggle. */ +.gh-portal-gift-checkout-tier-benefits { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.3s ease; + /* Clip the grid item: even with min-height: 0, the inner's padding still + renders ~20px tall, which Chrome leaks the first benefit through. */ + overflow: hidden; +} + +.gh-portal-gift-checkout-tier-benefits[data-open="true"] { + grid-template-rows: 1fr; +} + +.gh-portal-gift-checkout-tier-benefits-inner { + /* min-height: 0 overrides the default min-height: auto on grid items. + Padding on this element contributes to the grid track's min-size + (Chrome resolves 0fr to the inner padding-bottom otherwise), so we + keep the inner padding-free and put the spacing on the benefits + list instead. */ + min-height: 0; + overflow: hidden; +} + +.gh-portal-gift-checkout-tier-benefits-inner > .gh-portal-gift-checkout-benefits { + padding: 0 20px 20px 22px; +} + .gh-portal-gift-checkout-benefits { display: flex; flex-direction: column; - gap: 10px; - padding-left: 4px; + gap: 8px; } .gh-portal-gift-checkout-benefit { @@ -187,14 +246,38 @@ export const GiftPageStyles = ` flex-shrink: 0; } +/* Inside the brand-coloured right panel, lighten benefit text + checkmark + so they read against the saturated background. The checkmark SVG uses a + hard-coded stroke so we override it explicitly rather than via color. */ +.gh-portal-gift-checkout-right-panel .gh-portal-gift-checkout-benefit { + color: rgba(255, 255, 255, 0.85); +} + +.gh-portal-gift-checkout-right-panel .gh-portal-gift-checkout-benefit svg path { + stroke: rgba(255, 255, 255, 0.85); +} + .gh-portal-gift-checkout .gh-portal-btn-primary { border-radius: 999px; } +/* Sticky wrapper around the CTA: keeps the button visible at the bottom of + the viewport when the left column is long enough to scroll, and adds a + white→transparent fade at the top so scrolling content tucks under it + instead of cutting against the button edge. + Same pattern as .gh-portal-btn-container.sticky in frame.styles.js. */ +.gh-portal-gift-checkout-cta-wrapper { + position: sticky; + bottom: 0; + margin: 32px 0 -64px; + padding: 32px 0 64px; + background: linear-gradient(0deg, rgba(var(--whitergb), 1) 60%, rgba(var(--whitergb), 0) 100%); + z-index: 1; +} + .gh-portal-gift-checkout-cta { width: 100%; height: 48px; - margin-top: 32px; font-size: 1.5rem; font-weight: 600; } @@ -205,11 +288,21 @@ export const GiftPageStyles = ` align-self: start; height: 100vh; display: flex; + padding: 12px 12px 12px 0; + overflow-y: auto; +} + +/* Inner brand-coloured panel that holds the card. Inset 12px from top, right + and bottom of the right column (no left inset — flush with the column + boundary), with 32px rounded corners. */ +.gh-portal-gift-checkout-right-panel { + flex: 1; + display: flex; align-items: center; justify-content: center; - background: linear-gradient(to bottom, var(--white), color-mix(in srgb, var(--brandcolor) 5%, var(--white))); + background: var(--brandcolor); + border-radius: 32px; padding: 64px 48px 128px; - overflow-y: auto; } .gh-portal-gift-checkout-card-stack { @@ -234,13 +327,6 @@ export const GiftPageStyles = ` transform: rotate(3deg); } -.gh-portal-gift-checkout-card-benefits { - width: 100%; - margin-top: 28px; - overflow: hidden; - transition: height 0.3s ease; -} - .gh-portal-gift-checkout-details-toggle { display: inline-flex; align-items: center; @@ -249,7 +335,7 @@ export const GiftPageStyles = ` padding: 8px 12px; background: transparent; border: none; - color: var(--grey5); + color: rgba(255, 255, 255, 0.7); font-size: 1.4rem; font-weight: 500; cursor: pointer; @@ -257,7 +343,7 @@ export const GiftPageStyles = ` } .gh-portal-gift-checkout-details-toggle:hover { - color: var(--grey3); + color: rgba(255, 255, 255, 0.95); } .gh-portal-gift-checkout-details-toggle svg { @@ -284,29 +370,73 @@ export const GiftPageStyles = ` } .gh-portal-gift-checkout-details-inner { + min-height: 0; overflow: hidden; } +/* Card is split into two zones via a hard-stop gradient: the brand-coloured + top (~70%) holds the duration + tier in white text, the white bottom (~30%) + holds the site icon + name. flex-direction: column-reverse so the existing + site → meta DOM order maps to bottom → top visually. */ .gh-portal-gift-checkout-card { position: relative; width: 100%; max-width: 440px; aspect-ratio: 1.7 / 1; - background: linear-gradient(to top right, var(--white), color-mix(in srgb, var(--brandcolor) 8%, var(--white))); - border-radius: 32px; - padding: 28px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 24px 48px rgba(var(--blackrgb), 0.08), 0 4px 12px rgba(var(--blackrgb), 0.04); + background: linear-gradient(to bottom, color-mix(in srgb, var(--brandcolor) 70%, var(--white)) 75%, var(--white) 75%); + border-radius: 24px; + padding: 28px 28px 20px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 24px 48px rgba(var(--blackrgb), 0.08), 0 4px 12px rgba(var(--blackrgb), 0.04); display: flex; - flex-direction: column; + flex-direction: column-reverse; justify-content: space-between; overflow: hidden; transform-style: preserve-3d; will-change: transform; } -.gh-portal-gift-checkout-card-site { +/* Soft orb glow layer on the brand surface. Sits behind the content + (z-index 0) and is clipped to the brand area only (bottom: 25%) so it + doesn't bleed into the white strip. */ +.gh-portal-gift-checkout-card::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 25%; + background-image: url("${giftCardOrbUrl}"); + background-size: 100% auto; + background-position: 100px 100%; + background-repeat: no-repeat; + pointer-events: none; + z-index: 0; + opacity: 0.4; +} + +/* Make sure the card content paints above the orb glow. */ +.gh-portal-gift-checkout-card > * { position: relative; + z-index: 1; +} + +/* Lanyard slot — small semi-transparent pill at the top center of the card, + so the brand-coloured top reads as an ID-badge surface. */ +.gh-portal-gift-checkout-card::before { + content: ''; + position: absolute; + top: 16px; + left: 50%; + width: 56px; + height: 8px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.25); + transform: translateX(-50%); z-index: 2; +} + + +.gh-portal-gift-checkout-card-site { display: flex; align-items: center; gap: 8px; @@ -315,7 +445,6 @@ export const GiftPageStyles = ` .gh-portal-gift-checkout-card-site-icon { width: 24px; height: 24px; - border-radius: 5px; object-fit: cover; } @@ -326,15 +455,10 @@ export const GiftPageStyles = ` color: var(--grey0); } -.gh-portal-gift-checkout-card-meta { - position: relative; - z-index: 2; -} - .gh-portal-gift-checkout-card-duration { font-size: 2.6rem; font-weight: 600; - color: var(--grey0); + color: var(--white); letter-spacing: -0.01em; line-height: 1.1; } @@ -342,46 +466,10 @@ export const GiftPageStyles = ` .gh-portal-gift-checkout-card-tier { margin-top: 6px; font-size: 1.4rem; - color: var(--grey3); + color: rgba(255, 255, 255, 0.8); line-height: 1.3; } -/* Wrapped-present cross ribbon: vertical + horizontal straps, bow at intersection */ -.gh-portal-gift-checkout-card-ribbon-v, -.gh-portal-gift-checkout-card-ribbon-h { - position: absolute; - background: var(--brandcolor); - opacity: 0.3; - z-index: 1; -} - -.gh-portal-gift-checkout-card-ribbon-v { - top: 0; - bottom: 0; - right: 22%; - width: 12px; -} - -.gh-portal-gift-checkout-card-ribbon-h { - left: 0; - right: 0; - top: 32%; - height: 12px; - transform: translateY(-50%); -} - -.gh-portal-gift-checkout-card-bow { - position: absolute; - top: calc(32% - 42px); - right: calc(22% - 40px); - width: 90px; - height: 86px; - z-index: 2; - color: var(--brandcolor); - pointer-events: none; - filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.06)) drop-shadow(0 16px 32px rgba(0, 0, 0, 0.06)); -} - @media (max-width: 880px) { .gh-portal-gift-checkout { @@ -395,10 +483,14 @@ export const GiftPageStyles = ` order: -1; position: static; height: auto; - padding: 32px 24px; + padding: 12px 12px 0; overflow: visible; } + .gh-portal-gift-checkout-right-panel { + padding: 32px 24px; + } + .gh-portal-gift-checkout-left { padding: 32px 24px 80px; } @@ -467,21 +559,39 @@ const GiftPage = () => { const {site, brandColor, action, doAction} = useContext(AppContext); const [selectedInterval, setSelectedInterval] = useState(null); const [selectedProductId, setSelectedProductId] = useState(null); - const benefitsInnerRef = useRef(null); - const [benefitsHeight, setBenefitsHeight] = useState(undefined); const {cardRef, containerProps: cardTiltProps} = useCardTilt(); - - useEffect(() => { - const node = benefitsInnerRef.current; - if (!node || typeof ResizeObserver === 'undefined') { + const leftRef = useRef(null); + const innerRef = useRef(null); + const centeringDoneRef = useRef(false); + + // On first paint, vertically center the inner content within the left + // column by computing the available space and pushing the inner down by + // half. After this single measurement we never recompute — so when the + // benefits change height on tier switch, only the bottom of the column + // (the CTA) shifts, leaving the title and tier picker anchored. + useLayoutEffect(() => { + if (centeringDoneRef.current) { return; } - const observer = new ResizeObserver((entries) => { - setBenefitsHeight(entries[0].contentRect.height); - }); - observer.observe(node); - return () => observer.disconnect(); - }, []); + const inner = innerRef.current; + const left = leftRef.current; + if (!inner || !left) { + return; + } + const leftRect = left.getBoundingClientRect(); + if (leftRect.height === 0) { + return; + } + const leftStyle = window.getComputedStyle(left); + const pTop = parseFloat(leftStyle.paddingTop); + const pBottom = parseFloat(leftStyle.paddingBottom); + const available = leftRect.height - pTop - pBottom; + const space = available - inner.getBoundingClientRect().height; + if (space > 0) { + inner.style.marginTop = `${space / 2}px`; + } + centeringDoneRef.current = true; + }); if (!site) { return ; @@ -536,9 +646,9 @@ const GiftPage = () => {
-
+
-
-
-
- {siteIcon && ( - - )} - {siteTitle} -
-
-
{getDurationLabel(activeInterval)}
-
{activeProduct.name}
-
-
-
-
-
-
- {siteIcon && ( - - )} - {siteTitle} -
-
-
{getGiftDurationLabel(gift)}
-
{gift.tier.name}
+
+
+
+
+
+ {siteIcon && ( + + )} + {siteTitle} +
+
+
{getGiftDurationLabel(gift)}
+ {/* eslint-disable-next-line i18next/no-literal-string -- copy not yet finalised */} +
{`${gift.tier.name} membership`}
+
- - {benefits.length > 0 && ( - <> -
-
-
- {benefits.map((benefit, index) => { - const benefitName = typeof benefit === 'string' ? benefit : benefit?.name; - const benefitKey = typeof benefit === 'string' ? benefit : benefit?.id || `gift-benefit-${index}`; - - if (!benefitName) { - return null; - } - - return ( -
- - {benefitName} -
- ); - })} + {benefits.length > 0 && ( + <> +
+
+
+ {benefits.map((benefit, index) => { + const benefitName = typeof benefit === 'string' ? benefit : benefit?.name; + const benefitKey = typeof benefit === 'string' ? benefit : benefit?.id || `gift-benefit-${index}`; + + if (!benefitName) { + return null; + } + + return ( +
+ + {benefitName} +
+ ); + })} +
-
- - - )} + + + )} +
diff --git a/apps/portal/src/components/pages/gift-success-page.js b/apps/portal/src/components/pages/gift-success-page.js index 44b5c283ca9..f402aa9e0b8 100644 --- a/apps/portal/src/components/pages/gift-success-page.js +++ b/apps/portal/src/components/pages/gift-success-page.js @@ -146,61 +146,58 @@ const GiftSuccessPage = () => {
-
-
-
-
- {siteIcon && ( - +
+
+
+
+
+ {siteIcon && ( + + )} + {siteTitle} +
+ {tier && cadence && ( +
+
{getDurationLabel(cadence)}
+
{`${tier.name} membership`}
+
)} - {siteTitle}
- {tier && cadence && ( -
-
{getDurationLabel(cadence)}
-
{tier.name}
-
- )} - - {tier && tier.benefits && tier.benefits.length > 0 && ( - <> -
-
-
- {tier.benefits.map((benefit, idx) => { - const key = benefit?.id || `benefit-${idx}`; - return ( -
- - {benefit.name} -
- ); - })} + {tier && tier.benefits && tier.benefits.length > 0 && ( + <> +
+
+
+ {tier.benefits.map((benefit, idx) => { + const key = benefit?.id || `benefit-${idx}`; + return ( +
+ + {benefit.name} +
+ ); + })} +
-
- - - )} + + + )} +
diff --git a/apps/portal/src/components/pages/magic-link-page.js b/apps/portal/src/components/pages/magic-link-page.js index 158a8746853..56a5c0a01b0 100644 --- a/apps/portal/src/components/pages/magic-link-page.js +++ b/apps/portal/src/components/pages/magic-link-page.js @@ -342,66 +342,64 @@ export default class MagicLinkPage extends React.Component {
-
-
-
-
- {siteIcon && ( - - )} - {siteTitle} -
-
-
{getGiftDurationLabel(gift)}
-
{gift.tier?.name}
+
+
+
+
+
+ {siteIcon && ( + + )} + {siteTitle} +
+
+
{getGiftDurationLabel(gift)}
+ {/* eslint-disable-next-line i18next/no-literal-string -- copy not yet finalised */} +
{`${gift.tier?.name} membership`}
+
- - {benefits.length > 0 && ( - <> -
-
-
- {benefits.map((benefit, index) => { - const benefitName = typeof benefit === 'string' ? benefit : benefit?.name; - const benefitKey = typeof benefit === 'string' ? benefit : benefit?.id || `gift-benefit-${index}`; - - if (!benefitName) { - return null; - } - - return ( -
- - {benefitName} -
- ); - })} + {benefits.length > 0 && ( + <> +
+
+
+ {benefits.map((benefit, index) => { + const benefitName = typeof benefit === 'string' ? benefit : benefit?.name; + const benefitKey = typeof benefit === 'string' ? benefit : benefit?.id || `gift-benefit-${index}`; + + if (!benefitName) { + return null; + } + + return ( +
+ + {benefitName} +
+ ); + })} +
-
- - - )} + + + )} +
diff --git a/apps/portal/src/images/gift-card-orb.svg b/apps/portal/src/images/gift-card-orb.svg new file mode 100644 index 00000000000..f1e183c5c79 --- /dev/null +++ b/apps/portal/src/images/gift-card-orb.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/portal/test/unit/components/pages/AccountHomePage/paid-account-actions.test.js b/apps/portal/test/unit/components/pages/AccountHomePage/paid-account-actions.test.js index e9e82624937..7e12244c074 100644 --- a/apps/portal/test/unit/components/pages/AccountHomePage/paid-account-actions.test.js +++ b/apps/portal/test/unit/components/pages/AccountHomePage/paid-account-actions.test.js @@ -674,6 +674,71 @@ describe('PaidAccountActions', () => { }); }); + describe('Billing section', () => { + test('renders for a regular paid member', () => { + const products = getProductsData({numOfProducts: 1}); + const site = getSiteData({products, portalProducts: products.map(p => p.id)}); + const member = getMemberData({ + paid: true, + subscriptions: [ + getSubscriptionData({ + status: 'active', + amount: 500, + currency: 'USD', + interval: 'month' + }) + ] + }); + + const {container} = setup({site, member}); + + expect(container.querySelector('[data-test-button="manage-billing"]')).toBeInTheDocument(); + }); + + test('does not render for a gift member', () => { + const products = getProductsData({numOfProducts: 1}); + const site = getSiteData({products, portalProducts: products.map(p => p.id)}); + const member = getMemberData({ + paid: true, + status: 'gift', + subscriptions: [ + getSubscriptionData({ + status: 'active', + amount: 0, + currency: 'USD', + interval: 'month', + tier: {id: products[0].id, expiry_at: new Date('2099-01-01T12:00:00.000Z')} + }) + ] + }); + + const {container} = setup({site, member}); + + expect(container.querySelector('[data-test-button="manage-billing"]')).not.toBeInTheDocument(); + }); + + test('does not render for a complimentary member', () => { + const products = getProductsData({numOfProducts: 1}); + const site = getSiteData({products, portalProducts: products.map(p => p.id)}); + const member = getMemberData({ + paid: true, + status: 'comped', + subscriptions: [ + getSubscriptionData({ + status: 'active', + amount: 0, + currency: 'USD', + interval: 'month' + }) + ] + }); + + const {container} = setup({site, member}); + + expect(container.querySelector('[data-test-button="manage-billing"]')).not.toBeInTheDocument(); + }); + }); + describe('Canceled badge', () => { test('displays CANCELED badge when cancel_at_period_end is true', () => { const products = getProductsData({numOfProducts: 1}); diff --git a/e2e/tests/admin/onboarding.test.ts b/e2e/tests/admin/onboarding.test.ts index 3249c152983..cdc8cc278c5 100644 --- a/e2e/tests/admin/onboarding.test.ts +++ b/e2e/tests/admin/onboarding.test.ts @@ -7,9 +7,9 @@ type ChecklistState = 'pending' | 'started' | 'completed' | 'dismissed'; const allSteps = ['customize-design', 'first-post', 'build-audience', 'share-publication']; const activeStartedAt = '2026-05-01T00:00:00.000Z'; const navigationSteps: Array<[string, RegExp]> = [ - ['customize-design', /\/ghost\/#\/settings\/design\/edit\?ref=setup/], - ['first-post', /\/ghost\/#\/editor\/post/], - ['build-audience', /\/ghost\/#\/members/] + ['customize-design', /\/ghost\/(?:\?[^#]*)?#\/settings\/design\/edit\?ref=setup/], + ['first-post', /\/ghost\/(?:\?[^#]*)?#\/editor\/post/], + ['build-audience', /\/ghost\/(?:\?[^#]*)?#\/members/] ]; test.use({isolation: 'per-test'}); @@ -72,14 +72,15 @@ async function startOnboarding(page: Page) { } test.describe('Ghost Admin - Onboarding Checklist', () => { - test('new owner setup flow lands on onboarding', async ({page}) => { + test('firstStart root redirect lands on onboarding', async ({page}) => { await setOnboardingState(page, 'pending', ['customize-design']); - await page.goto('/ghost/#/setup/done'); + await page.goto('/ghost/#/?firstStart=true&sr_id=test-site&ssts=1778061878&ssml=test-signature'); const onboardingPage = new OnboardingPage(page); await expect(onboardingPage.checklist).toBeVisible(); await expectOnboardingRoute(page); + await expect(page).not.toHaveURL(/\/ghost\/#\/setup\/done/); const preferences = await getOnboardingPreferences(page); expect(preferences).toMatchObject({ diff --git a/e2e/tests/admin/signin.test.ts b/e2e/tests/admin/signin.test.ts index 0233c08524a..bc4d3ce0552 100644 --- a/e2e/tests/admin/signin.test.ts +++ b/e2e/tests/admin/signin.test.ts @@ -35,4 +35,27 @@ test.describe('Ghost Admin - Signin Redirect', () => { await postsPage.waitForPageToFullyLoad(); }); + + test('query params on a deep link survive signin redirect', async ({page, ghostAccountOwner}) => { + // Newsletter reply-to verification emails point at + // /settings/newsletters/?verifyEmail=. If the user clicks the + // link in a browser that isn't signed in, the param needs to survive + // the signin round-trip, otherwise the verify-on-mount handler in + // newsletters.tsx no-ops and the customer thinks their reply-to + // address didn't save (ONC-1618 / ONC-1642). + await logout(page); + + await page.goto('/ghost/#/settings/newsletters/?verifyEmail=fake-token-xyz'); + + const loginPage = new LoginPage(page); + await expect(loginPage.signInButton).toBeVisible(); + + await loginPage.signIn(ghostAccountOwner.email, ghostAccountOwner.password); + + // The error modal is the signal that the React verify handler ran: + // it only renders when newsletters.tsx attempted to redeem a token + // and the API rejected it (expected, since the token is fake). + await expect(page.getByRole('heading', {name: 'Error verifying email address'})).toBeVisible(); + expect(page.url()).toContain('verifyEmail=fake-token-xyz'); + }); }); diff --git a/ghost/admin/app/routes/setup/done.js b/ghost/admin/app/routes/setup/done.js index 0b4fce120f7..bf30488f10f 100644 --- a/ghost/admin/app/routes/setup/done.js +++ b/ghost/admin/app/routes/setup/done.js @@ -9,7 +9,7 @@ export default class SetupFinishingTouchesRoute extends AuthenticatedRoute { @service session; @service settings; - async beforeModel() { + async beforeModel(transition) { await super.beforeModel(...arguments); if (this.session.user.isOwnerOnly) { @@ -18,6 +18,7 @@ export default class SetupFinishingTouchesRoute extends AuthenticatedRoute { if (this.session.user?.isAdmin) { // The React admin app owns /setup/onboarding, so hand off via hash navigation. + transition.abort(); window.location.hash = '/setup/onboarding?returnTo=/analytics'; } } diff --git a/ghost/admin/app/services/session.js b/ghost/admin/app/services/session.js index 5ff594b8a65..6939cc26612 100644 --- a/ghost/admin/app/services/session.js +++ b/ghost/admin/app/services/session.js @@ -1,5 +1,7 @@ +import AuthConfiguration from 'ember-simple-auth/configuration'; import ESASessionService from 'ember-simple-auth/services/session'; import RSVP from 'rsvp'; +import windowProxy from 'ghost-admin/utils/window-proxy'; import {configureScope} from '@sentry/ember'; import {getOwner} from '@ember/application'; import {inject} from 'ghost-admin/decorators/inject'; @@ -91,7 +93,11 @@ export default class SessionService extends ESASessionService { const redirectUrl = window.sessionStorage.getItem('ghost-signin-redirect'); window.sessionStorage.removeItem('ghost-signin-redirect'); if (redirectUrl && !redirectUrl.startsWith('/signin') && !redirectUrl.startsWith('/signup') && !redirectUrl.startsWith('/setup')) { - this.router.transitionTo(redirectUrl); + // Hard navigate rather than router.transitionTo: the catch-all + // react-fallback route has no controller-declared queryParams, + // so transitionTo strips params like ?verifyEmail= used + // by newsletter reply-to confirmation links. + windowProxy.replaceLocation(`${AuthConfiguration.rootURL}#${redirectUrl}`); return; } diff --git a/ghost/admin/tests/acceptance/onboarding-test.js b/ghost/admin/tests/acceptance/onboarding-test.js index b0d9f87c057..fb3c082872e 100644 --- a/ghost/admin/tests/acceptance/onboarding-test.js +++ b/ghost/admin/tests/acceptance/onboarding-test.js @@ -13,6 +13,15 @@ describe('Acceptance: Onboarding', function () { // Helper selectors for better readability const checklist = () => find('[data-test-dashboard="onboarding-checklist"]'); + async function visitSetupDoneRedirect() { + try { + await visit('/setup/done'); + } catch (error) { + if (error?.message !== 'TransitionAborted') { + throw error; + } + } + } beforeEach(async function () { mockAnalyticsApps(); @@ -47,7 +56,7 @@ describe('Acceptance: Onboarding', function () { // the React analytics app, not Ember. it('setup/done starts onboarding and redirects to the React onboarding route', async function () { - await visit('/setup/done'); + await visitSetupDoneRedirect(); await waitUntil(() => window.location.hash === '#/setup/onboarding?returnTo=/analytics'); expect(window.location.hash).to.equal('#/setup/onboarding?returnTo=/analytics'); @@ -76,7 +85,7 @@ describe('Acceptance: Onboarding', function () { }); it('setup/done redirects to the React onboarding route without starting onboarding', async function () { - await visit('/setup/done'); + await visitSetupDoneRedirect(); await waitUntil(() => window.location.hash === '#/setup/onboarding?returnTo=/analytics'); expect(window.location.hash).to.equal('#/setup/onboarding?returnTo=/analytics'); diff --git a/ghost/core/core/server/services/stats/subscription-stats-service.js b/ghost/core/core/server/services/stats/subscription-stats-service.js index eb4aef974fe..36726dc6d54 100644 --- a/ghost/core/core/server/services/stats/subscription-stats-service.js +++ b/ghost/core/core/server/services/stats/subscription-stats-service.js @@ -186,6 +186,13 @@ class SubscriptionStatsService { .whereIn('status', ['consumed', 'expired', 'refunded']) .whereNotNull('redeemed_at') .whereRaw(`${cancellationDate} IS NOT NULL`) + // Exclude gifts that were consumed because the member upgraded + // to a paid subscription. In that case `consumed_at` is set + // before the gift's planned end (`consumes_at`). When the gift + // ends naturally via the cron job, `consumed_at` is always + // >= `consumes_at`, so this condition cleanly distinguishes + // upgrades (not churn) from natural endings (real churn). + .whereRaw(`NOT (status = 'consumed' AND consumed_at < consumes_at)`) .select(knex.raw(`DATE(${cancellationDate}) as date`)) .select('tier_id as tier') .select('cadence') diff --git a/ghost/core/core/server/web/gift-preview/controller.js b/ghost/core/core/server/web/gift-preview/controller.js index e1e40c80ceb..9187b21862c 100644 --- a/ghost/core/core/server/web/gift-preview/controller.js +++ b/ghost/core/core/server/web/gift-preview/controller.js @@ -1,9 +1,14 @@ const logging = require('@tryghost/logging'); const errors = require('@tryghost/errors'); const {generateGiftPreviewImage} = require('./image'); +const {t} = require('../../services/i18n'); function getCadenceLabel(cadence, duration) { - return duration === 1 ? `1 ${cadence}` : `${duration} ${cadence}s`; + if (cadence === 'year') { + return t('{count} year', {count: duration}); + } + + return t('{count} month', {count: duration}); } function escapeHtml(str) { @@ -44,8 +49,12 @@ async function giftPreview(req, res) { } const cadenceLabel = getCadenceLabel(gift.cadence, gift.duration); - const ogTitle = `You've been gifted ${cadenceLabel} of ${siteTitle}`; - const ogDescription = 'Open this link to redeem your gift.'; + const ogTitle = t(`You've been gifted {cadenceLabel} of {siteTitle}`, { + cadenceLabel, + siteTitle, + interpolation: {escapeValue: false} + }); + const ogDescription = t('Open this link to redeem your gift.'); const ogImage = `${siteUrl}/gift/${encodeURIComponent(token)}/image`; const ogUrl = `${siteUrl}/gift/${encodeURIComponent(token)}`; const redirectUrl = `${siteUrl}/#/portal/gift/redeem/${encodeURIComponent(token)}`; @@ -78,7 +87,7 @@ async function giftPreview(req, res) { - + `; diff --git a/ghost/core/test/unit/server/services/stats/subscriptions.test.js b/ghost/core/test/unit/server/services/stats/subscriptions.test.js index d6e4fb9a39b..a647fc0e338 100644 --- a/ghost/core/test/unit/server/services/stats/subscriptions.test.js +++ b/ghost/core/test/unit/server/services/stats/subscriptions.test.js @@ -46,6 +46,7 @@ describe('SubscriptionStatsService', function () { table.string('cadence'); table.string('status'); table.dateTime('redeemed_at'); + table.dateTime('consumes_at'); table.dateTime('consumed_at'); table.dateTime('expired_at'); table.dateTime('refunded_at'); @@ -404,12 +405,12 @@ describe('SubscriptionStatsService', function () { // Gifts covering each end-of-life branch: // - g-month: redeemed day 1, still active - // - g-year-consumed: redeemed day 1, consumed day 2 + // - g-year-consumed: redeemed day 1, period ended day 2 (natural end via cron) // - g-year-expired: redeemed day 1, expired day 3 // - g-month-refunded: redeemed day 1, refunded day 4 await db('gifts').insert([ {id: 'g-month', tier_id: 'basic', cadence: 'month', status: 'redeemed', redeemed_at: '1970-01-01T00:00:00.000Z'}, - {id: 'g-year-consumed', tier_id: 'basic', cadence: 'year', status: 'consumed', redeemed_at: '1970-01-01T00:00:00.000Z', consumed_at: '1970-01-02T00:00:00.000Z'}, + {id: 'g-year-consumed', tier_id: 'basic', cadence: 'year', status: 'consumed', redeemed_at: '1970-01-01T00:00:00.000Z', consumes_at: '1970-01-02T00:00:00.000Z', consumed_at: '1970-01-02T00:00:00.000Z'}, {id: 'g-year-expired', tier_id: 'basic', cadence: 'year', status: 'expired', redeemed_at: '1970-01-01T00:00:00.000Z', expired_at: '1970-01-03T00:00:00.000Z'}, {id: 'g-month-refunded', tier_id: 'basic', cadence: 'month', status: 'refunded', redeemed_at: '1970-01-01T00:00:00.000Z', refunded_at: '1970-01-04T00:00:00.000Z'} ]); @@ -484,6 +485,40 @@ describe('SubscriptionStatsService', function () { assert.equal(rows[0].positive_delta, 2); }); + it('Excludes consumed gifts where the member upgraded to paid (consumed_at < consumes_at) from cancellations', async function () { + const tiers = await createTiers(['basic']); + + // Two consumed gifts at the same tier/cadence on the same day: + // - g-upgrade: consumed BEFORE its planned end (consumed_at < consumes_at) → upgrade, NOT churn + // - g-natural: consumed AT/AFTER its planned end (consumed_at >= consumes_at) → real churn + await db('gifts').insert([ + {id: 'g-upgrade', tier_id: 'basic', cadence: 'month', status: 'consumed', redeemed_at: '1970-01-01T00:00:00.000Z', consumes_at: '1970-02-01T00:00:00.000Z', consumed_at: '1970-01-05T00:00:00.000Z'}, + {id: 'g-natural', tier_id: 'basic', cadence: 'month', status: 'consumed', redeemed_at: '1970-01-01T00:00:00.000Z', consumes_at: '1970-01-05T00:00:00.000Z', consumed_at: '1970-01-05T00:00:00.000Z'} + ]); + + // Add a paid signup so the service produces a non-empty result regardless. + const NEW = createEvent('created'); + await insertEvents([ + [NEW('A', tiers.basic.monthly)] + ]); + + const stats = new SubscriptionStatsService({knex: db}); + const result = await stats.getSubscriptionHistory(); + + // Day 5 monthly: only g-natural should count as a cancellation; g-upgrade is excluded. + const day5Cancellations = result.data + .filter(r => r.tier === 'basic' && r.cadence === 'month' && r.date === '1970-01-05') + .reduce((acc, r) => acc + r.cancellations, 0); + assert.equal(day5Cancellations, 1); + + // Both gifts still count as signups on day 1 (the upgrade was a real signup at the time). + const day1Signups = result.data + .filter(r => r.tier === 'basic' && r.cadence === 'month' && r.date === '1970-01-01') + .reduce((acc, r) => acc + r.signups, 0); + // 1 paid + 2 gift redemptions = 3 + assert.equal(day1Signups, 3); + }); + it('Excludes unredeemed gifts (expired/refunded before redemption) from cancellations', async function () { await createTiers(['basic']); diff --git a/ghost/core/test/unit/server/web/gift-preview/controller.test.js b/ghost/core/test/unit/server/web/gift-preview/controller.test.js index e2a07a6f951..76c4686909b 100644 --- a/ghost/core/test/unit/server/web/gift-preview/controller.test.js +++ b/ghost/core/test/unit/server/web/gift-preview/controller.test.js @@ -6,6 +6,11 @@ const urlUtils = require('../../../../../core/shared/url-utils'); const settingsCache = require('../../../../../core/shared/settings-cache'); const giftServiceWrapper = require('../../../../../core/server/services/gifts'); +// Initialise i18n before requiring the controller so its destructured `t` +// import resolves to the live i18next instance. The init helper falls back +// to 'en' when the locale setting isn't set. +require('../../../../../core/server/services/i18n').init(); + const controller = require('../../../../../core/server/web/gift-preview/controller'); describe('Gift Preview Controller', function () { diff --git a/ghost/i18n/locales/af/ghost.json b/ghost/i18n/locales/af/ghost.json index cd5577ca69c..4efc6507959 100644 --- a/ghost/i18n/locales/af/ghost.json +++ b/ghost/i18n/locales/af/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Alles van die beste!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Bevestig asseblief u e-posadres met hierdie skakel:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Veilige aanmeldskakel vir {siteTitle}", "See you soon!": "Sien u binnekort weer!", "Sent to {email}": "Gestuur na {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "U sal nie ingeskryf word nie.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "U is een kliek weg van inskrywing by {siteTitle} — bevestig asseblief u e-posadres met hierdie skakel:", "You're one tap away from subscribing to {siteTitle}!": "U is een kliek weg van inskrywing by {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/ar/ghost.json b/ghost/i18n/locales/ar/ghost.json index 62c71b716f2..bb0dfd1ae1b 100644 --- a/ghost/i18n/locales/ar/ghost.json +++ b/ghost/i18n/locales/ar/ghost.json @@ -1,4 +1,16 @@ { + "{count} month_zero": "", + "{count} month_one": "", + "{count} month_two": "", + "{count} month_few": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_zero": "", + "{count} year_one": "", + "{count} year_two": "", + "{count} year_few": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "مع خالص التقدير!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +44,12 @@ "Name": "الاسم", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "مدفوع", "Please confirm your email address with this link:": "برجاء تأكيد بريدك الاكتروني من خلال الرابط:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "رابط تسجيل الدخول الامن الى {siteTitle}", "See you soon!": "نراكم قريبا!", "Sent to {email}": "تم الارسال الى {email}", @@ -76,6 +90,7 @@ "You will not be subscribed.": "لم يتم اشتراكك.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "أنت على بعد خطوة من الاشتراك في {siteTitle} - برجاء تأكيد البريد الاكتروني من خلال هذا الرابط:", "You're one tap away from subscribing to {siteTitle}!": "أنت على بعد خطوة من الاشتراك في {siteTitle}", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "سيتم انتهاء فترتك التجريبية فى {date} وبحلول هذا الوقت سيتم محاسبتك على السعر الطبيعيى، بالطبع يمكنك إلغاء الاشتراك قبل هذا التاريخ.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "تم إلغاء اشتراكك وستنتهى صلاحية العضوية فى {date}. يمكنك تفعيل الاشتراك مرة أخرى من اعدادات حسابك.", "Your subscription has expired.": "انتهت صلاحية اشتراكك", diff --git a/ghost/i18n/locales/bg/ghost.json b/ghost/i18n/locales/bg/ghost.json index 3c4ba5e51cb..a24db6c9bc9 100644 --- a/ghost/i18n/locales/bg/ghost.json +++ b/ghost/i18n/locales/bg/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Сърдечни поздрави!", "Become a paid member of {site} to get access to all premium content.": "Станете плащащ абонат на {site} за да получите достъп до специалното съдържание за абонати.", @@ -32,10 +36,12 @@ "Name": "Име", "New comment on {postTitle}": "Нов коментар към {postTitle}", "New reply to your comment on {siteTitle}": "Нов отговор на вашия коментар в {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Или използвайте този линк, за да влезете по сигурен начин", "Or, skip the code and sign in directly": "Или пропуснете кода и влезте директно", "paid": "платен", "Please confirm your email address with this link:": "Моля, потвърдете своя имейл адрес чрез този линк:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Линк за сигурно влизане в сайта {siteTitle}", "See you soon!": "До скоро!", "Sent to {email}": "Изпратено до {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Няма да бъдете абонирани.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Вие сте на стъпка от абонирането си за {siteTitle} - моля, потвърдете имейла си, като ползвате този линк:", "You're one tap away from subscribing to {siteTitle}!": "Вие сте на една стъпка от това да се абонирате за {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Безплатният пробен период приключва на {date}, след което ще бъдете таксувани по редовната цена. Винаги може да се откажете преди това.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Вашият абонамент е прекратен и изтича на {date}. Може да възобновите абонамента си чрез настройките в профила си.", "Your subscription has expired.": "Абонаментът ви е изтекъл.", diff --git a/ghost/i18n/locales/bn/ghost.json b/ghost/i18n/locales/bn/ghost.json index 6c96c625698..3ae9ac1c80a 100644 --- a/ghost/i18n/locales/bn/ghost.json +++ b/ghost/i18n/locales/bn/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "শুভেচ্ছা!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "এই লিঙ্ক দিয়ে আপনার ইমেল ঠিকানা নিশ্চিত করুন:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} এর জন্য নিরাপদ সাইন ইন লিঙ্ক", "See you soon!": "শীঘ্রই দেখা হবে!", "Sent to {email}": "{email} এ পাঠানো হয়েছে", @@ -76,6 +82,7 @@ "You will not be subscribed.": "আপনি সাবস্ক্রাইব হবেন না।", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "আপনি {siteTitle} এ সাবস্ক্রাইব করতে এক ট্যাপ দূরে রয়েছেন — অনুগ্রহ করে এই লিঙ্ক দিয়ে আপনার ইমেল ঠিকানা নিশ্চিত করুন:", "You're one tap away from subscribing to {siteTitle}!": "আপনি {siteTitle} এ সাবস্ক্রাইব করতে এক ট্যাপ দূরে রয়েছেন!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/bs/ghost.json b/ghost/i18n/locales/bs/ghost.json index 4e1c4687f81..f40dceb9cd1 100644 --- a/ghost/i18n/locales/bs/ghost.json +++ b/ghost/i18n/locales/bs/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Lijep pozdrav!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +38,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Potvrdi svoju email adresu putem slijedećeg linka:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Siguran link za prijavu za {siteTitle}", "See you soon!": "Vidimo se uskoro!", "Sent to {email}": "Poslano na {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Proces pretplate će biti zaustavljen.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Jedan klik te dijeli od pretplate na {siteTitle} — potvrdi svoju email adresu putem slijedećeg linka:", "You're one tap away from subscribing to {siteTitle}!": "Jedan klik te dijeli od pretplate na {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/ca/ghost.json b/ghost/i18n/locales/ca/ghost.json index 1ee63a50e28..79b8ef17c21 100644 --- a/ghost/i18n/locales/ca/ghost.json +++ b/ghost/i18n/locales/ca/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Que et vagi bé!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +38,12 @@ "Name": "Nom", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "de pagament", "Please confirm your email address with this link:": "Si us plau, confirma la teva adreça de correu electrònic amb aquest enllaç:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Enllaç segur d'inici de sessió per {siteTitle}", "See you soon!": "A reveure!", "Sent to {email}": "Enviat a {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "No seràs subscrit.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Estàs a un sol pas de subscriure't a {siteTitle} — si us plau, confirma la teva adreça de correu electrònic amb aquest enllaç:", "You're one tap away from subscribing to {siteTitle}!": "Estàs a un sol pas de subscriure't a {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "El teu període de prova s'acaba el {date}. A partir d'aleshores, se't cobrarà el preu estipulat. Sempre pots cancel·lar la teva subscripció abans que això passi.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "La teva subscripció s'ha cancel·lat i expirarà el {date}. T'hi pots tornar a subscriure mitjançant la configuració del teu compte.", "Your subscription has expired.": "La teva subscripció ha expirat.", diff --git a/ghost/i18n/locales/context.json b/ghost/i18n/locales/context.json index 71a4f806bdc..1f8ae19e785 100644 --- a/ghost/i18n/locales/context.json +++ b/ghost/i18n/locales/context.json @@ -209,6 +209,7 @@ "Open Yahoo Mail": "Shown on signup and signin if your email is detected as Yahoo Mail", "Open email": "Shown on signup and signin if your email is detected, but we don't know which provider", "Open iCloud Mail": "Shown on signup and signin if your email is detected as iCloud Mail", + "Open this link to redeem your gift.": "Description shown in the social media (Open Graph) preview when a gift subscription link is shared", "Or use this link to securely sign in": "Text in the sign-in email above the magic link fallback, shown when a one-time code is provided", "Or, skip the code and sign in directly": "Text in the sign-in email offering a direct magic link as an alternative to entering the one-time code", "Permanent failure (bounce)": "A section title in the email suppression FAQ", @@ -226,6 +227,7 @@ "Price": "A label to indicate price of a tier", "Re-enable emails": "A button for members to turn-back-on emails, if they have been previously disabled as a result of delivery failures", "Recommendations": "A suggestion by/for another site of interest to users", + "Redeem your gift subscription": "Fallback link text shown on the gift preview page when JavaScript is disabled, used to redeem a gift subscription", "Renews at {price}.": "Portal - label for a subscription that renews at a specific price", "Replied to": "Comments label", "Reply": "Button to reply to a comment", @@ -381,6 +383,7 @@ "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "An error message displayed in the member account area when newsletter delivery to their address has repeatedly failed or they have marked an email as spam.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Signup confirmation email", "You're one tap away from subscribing to {siteTitle}!": "Signup confirmation email", + "You've been gifted {cadenceLabel} of {siteTitle}": "Title shown in the social media (Open Graph) preview when a gift subscription link is shared. {cadenceLabel} is a translated duration like '1 year' or '3 months'. {siteTitle} is the name of the publication", "You've successfully signed in.": "A notification displayed when the user signs in", "You've successfully subscribed to {siteTitle}": "Notification displayed after a successful signup. The ... tag wraps the site name. {siteTitle} is the name of the publication", "Your account": "A label indicating member account details", @@ -421,6 +424,10 @@ "{amount} off for first {number} months.": "Portal offer page, showing a discount", "{amount} off for first {period}.": "Portal offer page, showing a discount", "{amount} off forever.": "Portal offer page, showing a discount", + "{count} month_one": "Singular form of the month duration label used in the gift subscription Open Graph preview, e.g. '1 month'. i18next picks this form when {count} is 1 (English).", + "{count} month_other": "Plural form of the month duration label used in the gift subscription Open Graph preview, e.g. '3 months'. Some languages have additional plural forms (few, many) that translators can add as needed.", + "{count} year_one": "Singular form of the year duration label used in the gift subscription Open Graph preview, e.g. '1 year'. i18next picks this form when {count} is 1 (English).", + "{count} year_other": "Plural form of the year duration label used in the gift subscription Open Graph preview, e.g. '2 years'. Some languages have additional plural forms (few, many) that translators can add as needed.", "{date}": "This will appear at the top of the newsletter, as the publication date. No changes needed, usually.", "{discount}% discount": "Label for a discounted tier in portal", "{memberEmail} will no longer receive emails when someone replies to your comments.": "Shown when a member unsubscribes from comment replies", diff --git a/ghost/i18n/locales/cs/ghost.json b/ghost/i18n/locales/cs/ghost.json index e62ae78221d..8690f48a4d0 100644 --- a/ghost/i18n/locales/cs/ghost.json +++ b/ghost/i18n/locales/cs/ghost.json @@ -1,4 +1,12 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Ať se daří!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +40,12 @@ "Name": "Jméno", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "placený", "Please confirm your email address with this link:": "Prosím, potvrďte svou e-mailovou adresu kliknutím na následující odkaz:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Bezpečný přihlašovací odkaz pro {siteTitle}", "See you soon!": "Na viděnou!", "Sent to {email}": "Odesláno na {email}", @@ -76,6 +86,7 @@ "You will not be subscribed.": "Nebudete odebírat.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Stačí jedno kliknutí a odebíráte {siteTitle}. Potvrďte prosím svou e-mailovou adresu kliknutím na následující odkaz:", "You're one tap away from subscribing to {siteTitle}!": "Jste jen krok od odběru {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Vaše zkušební období končí {date}, poté Vám bude účtována běžná cena. Zkušební období můžete kdykoliv ukončit.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Vaše předplatné bylo zrušeno a vyprší {date}. Můžete si jej znovu obnovit v nastavení svého účtu.", "Your subscription has expired.": "Vaše předplatné vypršelo.", diff --git a/ghost/i18n/locales/da/ghost.json b/ghost/i18n/locales/da/ghost.json index 763085e2092..c4b644ec727 100644 --- a/ghost/i18n/locales/da/ghost.json +++ b/ghost/i18n/locales/da/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Alt det bedste!", "Become a paid member of {site} to get access to all premium content.": "Bliv betalende medlem af {site} for at få adgang til alt premiumindhold.", @@ -32,10 +36,12 @@ "Name": "Navn", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "betalt", "Please confirm your email address with this link:": "Bekræft venligst din e-mail med dette link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Sikker log ind link til {siteTitle}", "See you soon!": "Vi ses om lidt!", "Sent to {email}": "Sendt til {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Du bliver ikke tilmeldt noget.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Du er kun et klik væk fra at abonnere på {siteTitle} - bekræft venligst din e-mail med dette link:", "You're one tap away from subscribing to {siteTitle}!": "Du er kun et klik væk fra at abonnere på {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Din gratis prøveperiode slutter den {date}, hvorefter du vil blive opkrævet den normale pris. Du kan altid afmelde inden da.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Dit abonnement er blevet annulleret og udløber den {date}. Du kan genoptage dit abonnement via dine kontoindstillinger.", "Your subscription has expired.": "Dit abonnement er udløbet.", diff --git a/ghost/i18n/locales/de-CH/ghost.json b/ghost/i18n/locales/de-CH/ghost.json index dd1a2529aa3..b0fd39c3d2b 100644 --- a/ghost/i18n/locales/de-CH/ghost.json +++ b/ghost/i18n/locales/de-CH/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Alles Gute!", "Become a paid member of {site} to get access to all premium content.": "Werden Sie zahlendes Mitglied von {site}, um Zugang zu allen Premium-Inhalten zu erhalten.", @@ -32,10 +36,12 @@ "Name": "Name", "New comment on {postTitle}": "Neuer Kommentar zu {postTitle}", "New reply to your comment on {siteTitle}": "Neue Antwort auf Ihren Kommentar zu {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Oder verwenden Sie diesen Link, um sich sicher anzumelden", "Or, skip the code and sign in directly": "Oder überspringen Sie den Code und melden Sie sich direkt an", "paid": "zahlendes", "Please confirm your email address with this link:": "Bitte bestätigen Sie Ihre E-Mail-Adresse mit diesem Link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Sicherer Anmeldelink für unsere Seite «{siteTitle}»", "See you soon!": "Bis bald!", "Sent to {email}": "Gesendet an {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Sie werden nicht angemeldet.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Sie sind nur noch einen Klick von einem Abo unserer Seite «{siteTitle}» entfernt — bitte bestätigen Sie Ihre E-Mail-Adresse mit diesem Link:", "You're one tap away from subscribing to {siteTitle}!": "Sie sind nur einen Klick von einem Abo unserer Seite «{siteTitle}» entfernt!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Ihr Probeabo endet am {date}, ab dann wird Ihnen der reguläre Preis in Rechnung gestellt. Sie können Ihr Abo jederzeit zuvor kündigen.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Ihr Abo wurde gekündigt und läuft am {date} ab. Sie können Ihr Abo in den Kontoeinstellungen verlängern.", "Your subscription has expired.": "Ihr Abo ist abgelaufen.", diff --git a/ghost/i18n/locales/de/ghost.json b/ghost/i18n/locales/de/ghost.json index fd7a450854d..c411f22c3ac 100644 --- a/ghost/i18n/locales/de/ghost.json +++ b/ghost/i18n/locales/de/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Alles Gute!", "Become a paid member of {site} to get access to all premium content.": "Werde zahlendes Mitglied bei {site}, um Zugang zu allen Premium-Inhalten zu erhalten.", @@ -32,10 +36,12 @@ "Name": "Name", "New comment on {postTitle}": "Neuer Kommentar zu {postTitle}", "New reply to your comment on {siteTitle}": "Neue Antwort auf dein Kommentar bei {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Oder benutze diesen Link, um dich sicher einzuloggen", "Or, skip the code and sign in directly": "Oder vergiss den Code und logge dich direkt ein", "paid": "zahlendes", "Please confirm your email address with this link:": "Bitte bestätige deine E-Mail-Adresse mit diesem Link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Sicherer Anmeldelink für {siteTitle}", "See you soon!": "Bis bald!", "Sent to {email}": "Gesendet an {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Du wirst nicht angemeldet.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Du bist nur einen Klick von deinem Abonnement von {siteTitle} entfernt — bitte bestätige deine E-Mail-Adresse mit diesem Link:", "You're one tap away from subscribing to {siteTitle}!": "Du bist nur einen Klick von deinem Abonnement von {siteTitle} entfernt!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Dein kostenfreies Probeabo endet am {date}, ab dann wird der reguläre Preis in Rechnung gestellt. Du kannst jederzeit zuvor stornieren.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Dein Abo wurde storniert und endet am {date}. Du kannst dein Abo in den Kontoeinstellungen verlängern.", "Your subscription has expired.": "Dein Abo hat geendet.", diff --git a/ghost/i18n/locales/el/ghost.json b/ghost/i18n/locales/el/ghost.json index 73b43fabde9..f4549eac120 100644 --- a/ghost/i18n/locales/el/ghost.json +++ b/ghost/i18n/locales/el/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Ό,τι καλύτερο!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Παρακαλούμε επιβεβαιώστε τη διεύθυνση email σας με αυτόν τον σύνδεσμο:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Ασφαλής σύνδεσμος σύνδεσης για το {siteTitle}", "See you soon!": "Τα λέμε σύντομα!", "Sent to {email}": "Απεστάλη στο {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Δεν θα εγγραφείτε.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Είστε ένα πάτημα μακριά από την εγγραφή στο {siteTitle} — παρακαλούμε επιβεβαιώστε τη διεύθυνση email σας με αυτόν τον σύνδεσμο:", "You're one tap away from subscribing to {siteTitle}!": "Είστε ένα πάτημα μακριά από την εγγραφή στο {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/en/ghost.json b/ghost/i18n/locales/en/ghost.json index 9c1cbc8560a..9b9ea166826 100644 --- a/ghost/i18n/locales/en/ghost.json +++ b/ghost/i18n/locales/en/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "{count} month", + "{count} month_other": "{count} months", + "{count} year_one": "{count} year", + "{count} year_other": "{count} years", "{date}": "{date}", "All the best!": "", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "", "See you soon!": "", "Sent to {email}": "", @@ -76,6 +82,7 @@ "You will not be subscribed.": "", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "", "You're one tap away from subscribing to {siteTitle}!": "", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/eo/ghost.json b/ghost/i18n/locales/eo/ghost.json index 3654469d82c..bee102afb76 100644 --- a/ghost/i18n/locales/eo/ghost.json +++ b/ghost/i18n/locales/eo/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Ĉion bonan!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Bonvolu konfirmi vian retadreson per ĉi tiu ligilo:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Sekura ensaluta ligilo por {siteTitle}", "See you soon!": "Ĝis baldaŭ!", "Sent to {email}": "Sendita al {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Vi ne abonitos.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Vi estas unu premo for de abonado al {siteTitle} — bonvolu konfirmi vian retadreson per ĉi tiu ligilo:", "You're one tap away from subscribing to {siteTitle}!": "", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/es/ghost.json b/ghost/i18n/locales/es/ghost.json index 4226b52311d..b2348b430c6 100644 --- a/ghost/i18n/locales/es/ghost.json +++ b/ghost/i18n/locales/es/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "¡Todo lo mejor para ti!", "Become a paid member of {site} to get access to all premium content.": "Hazte miembro de pago de {site} para acceder a todo el contenido premium.", @@ -32,10 +38,12 @@ "Name": "Nombre", "New comment on {postTitle}": "Nuevo comentario en {postTitle}", "New reply to your comment on {siteTitle}": "Nueva respuesta a tu comentario en {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "de pago", "Please confirm your email address with this link:": "Por favor, confirma tu dirección de correo electrónico haciendo clic en este enlace:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Inicio de sesión seguro para {siteTitle}", "See you soon!": "¡Hasta pronto!", "Sent to {email}": "Enviado a {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "No serás suscrito.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Estás muy cerca de suscribirte a {siteTitle} — por favor confirma tu correo electrónico haciendo clic en este enlace:", "You're one tap away from subscribing to {siteTitle}!": "¡Estás muy cerca de suscribirte a {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Tu prueba gratuita termina el {date}, fecha en la que se te aplicará la cuota normal. Puedes cancelar en cualquier momento antes de esa fecha.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Tu suscripción ha sido cancelada y caducará el {date}. Puedes reactivar tu suscripción en la configuración de tu cuenta.", "Your subscription has expired.": "Tu suscripción ha caducado.", diff --git a/ghost/i18n/locales/et/ghost.json b/ghost/i18n/locales/et/ghost.json index acde5ab04e2..2acc76fede5 100644 --- a/ghost/i18n/locales/et/ghost.json +++ b/ghost/i18n/locales/et/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Kõike head!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Palun kinnita oma e-posti aadress selle lingiga:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Turvaline sisselogimislink {siteTitle} lehele", "See you soon!": "Kohtumiseni!", "Sent to {email}": "Saadetud aadressile {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Sind ei lisata jälgijate hulka.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Oled vaid ühe klõpsu kaugusel {siteTitle} jälgijaks hakkamisest — palun kinnita oma e-posti aadress selle lingiga:", "You're one tap away from subscribing to {siteTitle}!": "Oled vaid ühe klõpsu kaugusel {siteTitle} jälgijaks hakkamisest!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/eu/ghost.json b/ghost/i18n/locales/eu/ghost.json index 3b115c550ef..f2706cce79b 100644 --- a/ghost/i18n/locales/eu/ghost.json +++ b/ghost/i18n/locales/eu/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Zorte on!", "Become a paid member of {site} to get access to all premium content.": "Egin zaitez ordainpeko kide {site}(e)ko primerako eduki guztira sarbidea izateko.", @@ -32,10 +36,12 @@ "Name": "Izena", "New comment on {postTitle}": "Iruzkin berria {postTitle} argitalpenean", "New reply to your comment on {siteTitle}": "Erantzun berria {siteTitle}(e)ko iruzkinari", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "ordainpekoa", "Please confirm your email address with this link:": "Egiaztatu zure ePosta helbidea honako estekarekin:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle}(e)n saioa hasteko esteka segurua", "See you soon!": "Laster arte!", "Sent to {email}": "{email}(r)i bidali zaio", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Ez zaitugu harpidetuko.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Tap bakarra baino ez duzu behar {siteTitle}(e)ra harpidetzeko — egiaztatu zure ePosta helbidea honako estekan:", "You're one tap away from subscribing to {siteTitle}!": "Tap bakarra baino ez duzu behar {siteTitle}(e)ra harpidetzeko!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Doako probaldia {date}(e)an amaituko da, eta prezio arrunta kobratuko zaizu. Data hori baino lehen ezeztatu dezakezu.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Harpidetza utzi da eta {date}(e)an amaituko da. Berriro harpidetu zaitezke zure kontuaren ezarpenen bidez.", "Your subscription has expired.": "Zure harpidetza iraungi da.", diff --git a/ghost/i18n/locales/fa/ghost.json b/ghost/i18n/locales/fa/ghost.json index 2fe27863434..8c14eea3195 100644 --- a/ghost/i18n/locales/fa/ghost.json +++ b/ghost/i18n/locales/fa/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "با آرزوی بهترین\u200cها!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "خواهشمند است از طریق این پیوند آدرس ایمیل خود را تأیید کنید:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "پیوند امن ورود به وب\u200cسایت {siteTitle}", "See you soon!": "به زودی می\u200cبینمت!", "Sent to {email}": "ارسال شده به {email} ", @@ -76,6 +82,7 @@ "You will not be subscribed.": "شما مشترک نخواهید شد.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "شما در یک قدمی دریافت اشتراک وب\u200cسایت {siteTitle} هستید - خواهشمند است آدرس ایمیل خود را از طریق این لینک تأیید کنید:", "You're one tap away from subscribing to {siteTitle}!": "شما در یک قدمی دریافت اشتراک وب\u200cسایت {siteTitle} هستید!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/fi/ghost.json b/ghost/i18n/locales/fi/ghost.json index 9b592f5082c..46c424d8340 100644 --- a/ghost/i18n/locales/fi/ghost.json +++ b/ghost/i18n/locales/fi/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Kaikkea hyvää!", "Become a paid member of {site} to get access to all premium content.": "Saat kaiken sisällön käyttöösi, kun otat maksullisen jäsenyyden sivulle {site}.", @@ -32,10 +36,12 @@ "Name": "Nimi", "New comment on {postTitle}": "Uusi kommentti kirjoituksessa {postTitle}", "New reply to your comment on {siteTitle}": "Uusi vastaus kommenttiisi sivulla {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Tai voit turvallisesti kirjautua sisään käyttämällä seuraavaa linkkiä", "Or, skip the code and sign in directly": "Tai voit ohittaa koodin ja kirjautua suoraan sisään", "paid": "maksullinen ", "Please confirm your email address with this link:": "Vahvista sähköpostisi tästä linkistä:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Turvallinen kirjautumislinkki sivustolle {siteTitle}", "See you soon!": "Nähdään pian!", "Sent to {email}": "Lähetetty {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Sinulle ei tehdä tilausta", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Olet yhden klikkauksen päässä tilauksestasi sivulle {siteTitle} - Vahista sähköpostisi tästä:", "You're one tap away from subscribing to {siteTitle}!": "Olet yhden klikkauksen päässä tilauksestasi sivulle {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Ilmainen kokeilusi loppuu {date}, jolloin sinulta veloitetaan tilausmaksu. Voit aina peruuttaa tilauksesi ennen tätä.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Tilauksesi on peruutettu ja päättyy {date}. Voit jatkaa tilausta tilin asetuksissa.", "Your subscription has expired.": "Tilauksesi on päättynyt.", diff --git a/ghost/i18n/locales/fr/ghost.json b/ghost/i18n/locales/fr/ghost.json index 6da26d8cf10..50b5931936d 100644 --- a/ghost/i18n/locales/fr/ghost.json +++ b/ghost/i18n/locales/fr/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Bien à vous !", "Become a paid member of {site} to get access to all premium content.": "Devenez un(e) abonné(e) payant de {site} pour accéder à du contenu exclusif.", @@ -32,10 +38,12 @@ "Name": "Nom", "New comment on {postTitle}": "Nouveau commentaire sur {postTitle}", "New reply to your comment on {siteTitle}": "Nouvelle réponse à votre commentaire sur {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Ou utilisez ce lien pour vous connecter en toute sécurité ", "Or, skip the code and sign in directly": "Ou ignorez le code et connectez-vous directement ", "paid": "payé", "Please confirm your email address with this link:": "Veuillez confirmer votre adresse e-mail en cliquant sur ce lien :", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Lien sécurisé de connexion pour {siteTitle}", "See you soon!": "À bientôt !", "Sent to {email}": "Envoyé à {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Vous ne serez pas abonné(e).", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Plus qu’une étape pour vous abonner à {siteTitle} — confirmez votre adresse e-mail avec ce lien :", "You're one tap away from subscribing to {siteTitle}!": "Plus qu’une étape pour vous abonner à {siteTitle} !", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Votre essai gratuit se termine le {date}, après quoi le prix normal vous sera facturé. Vous pouvez toujours annuler avant cette date.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Votre abonnement a été annulé et expirera le {date}. Vous pouvez reprendre votre abonnement via les paramètres de votre compte.", "Your subscription has expired.": "Votre abonnement a expiré.", diff --git a/ghost/i18n/locales/gd/ghost.json b/ghost/i18n/locales/gd/ghost.json index 63fef0c84ee..70cec83977a 100644 --- a/ghost/i18n/locales/gd/ghost.json +++ b/ghost/i18n/locales/gd/ghost.json @@ -1,4 +1,12 @@ { + "{count} month_one": "", + "{count} month_two": "", + "{count} month_few": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_two": "", + "{count} year_few": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Meal do shlàinte!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +40,12 @@ "Name": "Ainm", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "pàighte", "Please confirm your email address with this link:": "Dearbhaich am post-d agad leis a' cheangal seo:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Ceangal tèarainte airson clàradh a-steach ris an làrach-lìn, {siteTitle}", "See you soon!": "Tìoraidh an-dràsta!", "Sent to {email}": "Cuir gu {email}", @@ -76,6 +86,7 @@ "You will not be subscribed.": "Cha fo-sgrìobh thu.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Chan eil ach aon cheum ri dhol a-nis gus fo-sgrìobhadh ris an làrach-lìn, {siteTitle}, dearbhaich am post-d agad leis a' cheangal seo:", "You're one tap away from subscribing to {siteTitle}!": "Chan eil ach aon cheum ri dhol a-nis gus fo-sgrìobhadh ris an làrach-lìn, {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Falbhaidh an ùine air an tionndadh triail agad {date}, agus bidh agad ris a' phrìs abhaisteach a phàigheadh. Faodaidh tu stad a chur air an fho-sgrìobhadh agad ron a sheo ge-tà.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Chaidh stad air chur air an fho-sgrìobhadh agad, agus falbhaidh an ùine air {date}. Faodaidh tu am fo-sgrìobhadh agad ath-thòiseachadh ann an roghainnean a' chunntais agad.", "Your subscription has expired.": "Dh'fhalbh an ùine air an fho-sgrìobhadh agad.", diff --git a/ghost/i18n/locales/he/ghost.json b/ghost/i18n/locales/he/ghost.json index 1b821b67a0a..45bf4b83726 100644 --- a/ghost/i18n/locales/he/ghost.json +++ b/ghost/i18n/locales/he/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_two": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_two": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "המשך יום טוב!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +38,12 @@ "Name": "שם", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "בתשלום", "Please confirm your email address with this link:": "אנא אשרו את כתובת המייל שלכם בעזרת הלינק הבא:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "לינק הרשמה מאובטח ל{siteTitle}", "See you soon!": "להתראות בקרוב!", "Sent to {email}": "נשלח אל {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "לא נרשום אתכם.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "אתם רק צעד אחד מההרשמה אל {siteTitle} — אנא אשרו את כתובת המייל שלכם בעזרת הלינק הבא:", "You're one tap away from subscribing to {siteTitle}!": "אתם רק צעד אחד מההרשמה אל {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "המינוי החינמי שלך יסתיים ב-{date}, ואז תחוייב במחיר הרגיל. תמיד תוכל לבטל לפני כן.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "המינוי שלך בוטל ויפוג ב-{date}. תוכל לשחזר את המינוי דרך ההגדרות של החשבון שלך.", "Your subscription has expired.": "פג תוקפו של המינוי שלך.", diff --git a/ghost/i18n/locales/hi/ghost.json b/ghost/i18n/locales/hi/ghost.json index 4554dd4d729..755196d6f42 100644 --- a/ghost/i18n/locales/hi/ghost.json +++ b/ghost/i18n/locales/hi/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "सभी को शुभकामनाएं!", "Become a paid member of {site} to get access to all premium content.": "{site} का सशुल्क सदस्य बनें ताकि आपको सभी प्रीमियम सामग्री तक पहुंच मिल सके।", @@ -32,10 +36,12 @@ "Name": "नाम", "New comment on {postTitle}": "{postTitle} पर नई टिप्पणी", "New reply to your comment on {siteTitle}": "{siteTitle} पर आपकी टिप्पणी का नया उत्तर", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "सशुल्क", "Please confirm your email address with this link:": "कृपया इस लिंक से अपना ईमेल पता पुष्टि करें:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} के लिए सुरक्षित साइन इन लिंक", "See you soon!": "जल्द ही मिलते हैं!", "Sent to {email}": "{email} पर भेजा गया", @@ -76,6 +82,7 @@ "You will not be subscribed.": "आप सदस्यता नहीं लेंगे।", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "आप {siteTitle} की सदस्यता लेने से एक टैप दूर हैं — कृपया इस लिंक से अपना ईमेल पता पुष्टि करें:", "You're one tap away from subscribing to {siteTitle}!": "आप {siteTitle} की सदस्यता लेने से एक टैप दूर हैं!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "आपकी नि:शुल्क परीक्षण अवधि {date} को समाप्त होगी, जिसके बाद सामान्य शुल्क लिया जाएगा। आप इससे पहले कभी भी रद्द कर सकते हैं।", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "आपकी सदस्यता रद्द कर दी गई है और {date} को समाप्त होगी। आप अपनी खाता सेटिंग्स के माध्यम से इसे फिर से सक्रिय कर सकते हैं।", "Your subscription has expired.": "आपकी सदस्यता समाप्त हो चुकी है।", diff --git a/ghost/i18n/locales/hr/ghost.json b/ghost/i18n/locales/hr/ghost.json index e9d5eb7e85f..181a17acbf5 100644 --- a/ghost/i18n/locales/hr/ghost.json +++ b/ghost/i18n/locales/hr/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Sve najbolje!", "Become a paid member of {site} to get access to all premium content.": "Postanite pretplatnik na {site} kako biste dobili pristup svim premijum sadržajima.", @@ -32,10 +38,12 @@ "Name": "Ime", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "plaćeno", "Please confirm your email address with this link:": "Molimo vas da potvrdite adresu e-pošte klikom na ovaj link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Sigurni link za prijavu na {siteTitle}", "See you soon!": "Vidimo se uskoro!", "Sent to {email}": "Poslano na {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Nećete biti pretplaćeni.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Na korak ste od pretplate na {siteTitle} - molimo vas da potvrdite adresu e-pošte na ovom linku:", "You're one tap away from subscribing to {siteTitle}!": "Na korak ste od pretplate na {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Vaš besplatni probni period se završava na dan {date}, nakon čega će vam biti naplaćena redovna cijena. Uvijek možete otkazati prije toga.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Vaša pretplata je otkazana i isteći će na dan {date}. Možete je ponovno aktivirati putem postavki računa.", "Your subscription has expired.": "Vaša pretplata je istekla.", diff --git a/ghost/i18n/locales/hu/ghost.json b/ghost/i18n/locales/hu/ghost.json index 0419a08d69b..c73f475312d 100644 --- a/ghost/i18n/locales/hu/ghost.json +++ b/ghost/i18n/locales/hu/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Üdvözlettel,", "Become a paid member of {site} to get access to all premium content.": "Legyél fizető tag {site} oldalon, hogy hozzáférj minden prémium tartalomhoz.", @@ -32,10 +36,12 @@ "Name": "Név", "New comment on {postTitle}": "Új hozzászólás a {postTitle} bejegyzéshez", "New reply to your comment on {siteTitle}": "Új válasz az egyik hozzászólásodra a {siteTitle} oldalon", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "fizetve", "Please confirm your email address with this link:": "Kattints az alábbi linkre az e-mail-címed megerősítéséért:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} — Biztonságos bejelentkezési link", "See you soon!": "Üdv!", "Sent to {email}": "Elküldve ide: {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Nem leszel feliratkozva.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Csak egy kattintásra vagy a feliratkozástól {siteTitle} oldalra! Kérjük, hagyd jóvá az e-mail-címedet az alábbi linkkel:", "You're one tap away from subscribing to {siteTitle}!": "{siteTitle} — Csak egy kattintásra vagy a feliratkozástól!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Az ingyenes próbaidőszak után {date} fiókod automatikusan átvált előfizetésesre. A próbaidőszak alatt bármikor lemondhatod az előfizetésedet.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Az előfizetésed törölve lesz és lejár ekkor: {date}. A fiókodban beállíthatod az előfizetésed folytatását.", "Your subscription has expired.": "Az előfizetésed lejárt.", diff --git a/ghost/i18n/locales/id/ghost.json b/ghost/i18n/locales/id/ghost.json index 7742a59d45c..ec5f8d39806 100644 --- a/ghost/i18n/locales/id/ghost.json +++ b/ghost/i18n/locales/id/ghost.json @@ -1,4 +1,6 @@ { + "{count} month_other": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Semoga sukses!", "Become a paid member of {site} to get access to all premium content.": "Jadilah anggota berbayar {site} untuk mendapatkan akses ke semua konten premium.", @@ -32,10 +34,12 @@ "Name": "Nama", "New comment on {postTitle}": "Komentar baru pada {postTitle}", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "berbayar", "Please confirm your email address with this link:": "Harap konfirmasikan alamat email Anda dengan tautan ini:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Tautan masuk yang aman untuk {siteTitle}", "See you soon!": "Sampai jumpa lagi!", "Sent to {email}": "Dikirim ke {email}", @@ -76,6 +80,7 @@ "You will not be subscribed.": "Anda tidak akan berlangganan.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Anda hanya perlu satu kali ketuk untuk berlangganan ke {siteTitle} — harap konfirmasikan alamat email Anda dengan tautan ini:", "You're one tap away from subscribing to {siteTitle}!": "Anda hanya perlu satu kali ketuk untuk berlangganan ke {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Masa coba gratis Anda akan berhenti pada {date}, setelah itu Anda akan dikenakan harga reguler. Anda dapat membatalkan langganan sebelum tanggal tersebut.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Status berlangganan Anda telah dibatalkan dan akan berakhir pada {date}. Anda dapat melanjutkan berlangganan melalui pengaturan akun Anda.", "Your subscription has expired.": "Status berlangganan Anda telah kadaluwarsa.", diff --git a/ghost/i18n/locales/is/ghost.json b/ghost/i18n/locales/is/ghost.json index 73f1225f54c..40634ab14da 100644 --- a/ghost/i18n/locales/is/ghost.json +++ b/ghost/i18n/locales/is/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Bestu kveðjur!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Vinsamlegast staðfestið netfangið með þessum hlekki", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Innskráningarhlekkur fyrir {siteTitle}", "See you soon!": "Sjáumst von bráðar!", "Sent to {email}": "Senda til {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Þú verður ekki áskrifandi.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Þú ert einum smelli frá áskrift að {siteTitle} — vinsamlegast staðfestið netfang með þessum hlekki:", "You're one tap away from subscribing to {siteTitle}!": "Þú ert einum smelli frá áskrift að {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/it/ghost.json b/ghost/i18n/locales/it/ghost.json index b4d1e395f96..ad019696862 100644 --- a/ghost/i18n/locales/it/ghost.json +++ b/ghost/i18n/locales/it/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "A presto!", "Become a paid member of {site} to get access to all premium content.": "Abbonati a {site} per accedere a tutti i contenuti premium.", @@ -32,10 +38,12 @@ "Name": "Nome", "New comment on {postTitle}": "Nuovo commento su {postTitle}", "New reply to your comment on {siteTitle}": "Nuova risposta al tuo commento su {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Oppure usa questo link per accedere in modo sicuro", "Or, skip the code and sign in directly": "Oppure, accedi direttamente senza il codice", "paid": "a pagamento", "Please confirm your email address with this link:": "Conferma il tuo indirizzo email a questo link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Link per l'accesso sicuro a {siteTitle}", "See you soon!": "A presto!", "Sent to {email}": "Inviata a {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Non sarai iscritto.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Sei a un passo dall'abbonamento a {siteTitle} — conferma la tua email a questo link:", "You're one tap away from subscribing to {siteTitle}!": "Sei a un passo dall'abbonamento a {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "La tua prova gratuita finisce il {date}, pagherai solo allora. Puoi cancellare l’abbonamento prima.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Il tuo abbonamento è stato annullato e scadrà il {date}. Puoi riattivare l'abbonamento dalle impostazioni del tuo account.", "Your subscription has expired.": "Il tuo abbonamento è scaduto.", diff --git a/ghost/i18n/locales/ja/ghost.json b/ghost/i18n/locales/ja/ghost.json index 4e02194134a..06c8d4ca70b 100644 --- a/ghost/i18n/locales/ja/ghost.json +++ b/ghost/i18n/locales/ja/ghost.json @@ -1,4 +1,6 @@ { + "{count} month_other": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "よろしくお願いします!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +34,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "このリンクを使用してメールアドレスを確認してください:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle}へのログインリンクです", "See you soon!": "引き続きよろしくお願いします。", "Sent to {email}": "{email}に送信", @@ -76,6 +80,7 @@ "You will not be subscribed.": "購読されません。", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "{siteTitle}への購読まであと少しです。このリンクを使用してメールアドレスを確認してください。", "You're one tap away from subscribing to {siteTitle}!": "{siteTitle}への購読まであと少しです。", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/ko/ghost.json b/ghost/i18n/locales/ko/ghost.json index 39d7667112e..85a18dc0017 100644 --- a/ghost/i18n/locales/ko/ghost.json +++ b/ghost/i18n/locales/ko/ghost.json @@ -1,4 +1,6 @@ { + "{count} month_other": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "모든 것이 잘 되기를 바라요!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +34,12 @@ "Name": "이름", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "유료", "Please confirm your email address with this link:": "아래 링크를 사용하여 이메일 주소를 확인해 주세요:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle}의 안전한 로그인 링크예요.", "See you soon!": "곧 다시 만나요!", "Sent to {email}": "{email}으로 전송되었어요.", @@ -76,6 +80,7 @@ "You will not be subscribed.": "구독되지 않을 것이에요.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "탭하시면 {siteTitle} 구독이 완료돼요! 이 링크를 사용하여 이메일 주소를 확인해 주세요.", "You're one tap away from subscribing to {siteTitle}!": "탭하시면 {siteTitle} 구독이 완료돼요!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "무료 체험은 {date}에 종료되며, 이후 정상 가격이 청구돼요. 그 전에 언제든 취소할 수 있어요.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "구독이 취소되었으며 {date}에 만료돼요. 계정 설정에서 구독을 다시 시작할 수 있어요.", "Your subscription has expired.": "구독이 만료되었어요.", diff --git a/ghost/i18n/locales/kz/ghost.json b/ghost/i18n/locales/kz/ghost.json index 17aff2e9330..00d0ca77b01 100644 --- a/ghost/i18n/locales/kz/ghost.json +++ b/ghost/i18n/locales/kz/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Ізгі ниетпен!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Email поштаңызды осы сілтеме арқылы растауыңызды сұраймыз:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} сайтына қауіпсіз кіруге арналған сілтеме", "See you soon!": "Көріскенше!", "Sent to {email}": "{email} поштасына жіберілді", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Сіз үшін жазылым жасамаймыз.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "{siteTitle} жазылу үшін бір-ақ қадам қалды — мына сілтеме арқылы email адресіңізді растауыңызды сұраймыз:", "You're one tap away from subscribing to {siteTitle}!": "{siteTitle} жазылу үшін бір-ақ қадам қалды!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/lt/ghost.json b/ghost/i18n/locales/lt/ghost.json index 3eef0004d2d..eefcb35f301 100644 --- a/ghost/i18n/locales/lt/ghost.json +++ b/ghost/i18n/locales/lt/ghost.json @@ -1,4 +1,12 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Viso geriausio!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +40,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Patvirtinkite savo el. pašto adresą su šia nuoroda:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Saugi prisijungimo nuoroda į {siteTitle}", "See you soon!": "Iki pasimatymo!", "Sent to {email}": "Išsiųsta į {email}", @@ -76,6 +86,7 @@ "You will not be subscribed.": "Jūs netapsite prenumeratoriumi.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Esate per vieną paspaudimą nuo {siteTitle} prenumeratos - patvirtinkite savo el. pašto adresą su šia nuoroda:", "You're one tap away from subscribing to {siteTitle}!": "Esate per vieną paspaudimą nuo {siteTitle} prenumeratos!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/lv/ghost.json b/ghost/i18n/locales/lv/ghost.json index f23d771f810..a7f8854d73b 100644 --- a/ghost/i18n/locales/lv/ghost.json +++ b/ghost/i18n/locales/lv/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_zero": "", + "{count} month_one": "", + "{count} month_other": "", + "{count} year_zero": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Visu to labāko!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +38,12 @@ "Name": "Vārds", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "samaksāts", "Please confirm your email address with this link:": "Lūdzu, apstipriniet savu e-pasta adresi, izmantojot šo saiti:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Droša pierakstīšanās saite vietnei {siteTitle}", "See you soon!": "Uz drīzu tikšanos!", "Sent to {email}": "Nosūtīts uz {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Jūs netiksiet abonēts.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Atliek tikai viens pieskāriens, lai abonētu vietni {siteTitle} — lūdzu, apstipriniet savu e-pasta adresi, izmantojot šo saiti:", "You're one tap away from subscribing to {siteTitle}!": "Atliek tikai viens pieskāriens, lai abonētu vietni {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Jūsu bezmaksas izmēģinājuma periods beidzas {date}, un tad no jums tiks iekasēta parastā cena. Jūs vienmēr varat atcelt pirms tam.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Jūsu abonements ir atcelts, un tā derīguma termiņš beigsies {date}. Varat atsākt abonementu, izmantojot sava konta iestatījumus.", "Your subscription has expired.": "Jūsu abonements ir beidzies.", diff --git a/ghost/i18n/locales/mk/ghost.json b/ghost/i18n/locales/mk/ghost.json index bb53fb9ce08..1f118710d6c 100644 --- a/ghost/i18n/locales/mk/ghost.json +++ b/ghost/i18n/locales/mk/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Сѐ најдобро!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Потврдете ја вашата email адреса со овој линк:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Безбеден линк за најава на {siteTitle}", "See you soon!": "Ќе се видиме наскоро!", "Sent to {email}": "Испратете на {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Вие нема да бидете претплатени.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Вие сте на само еден клик до претплатување на {siteTitle} - потврдете ја вашата email адреса со овој линк:", "You're one tap away from subscribing to {siteTitle}!": "Вие сте на само еден клик до претплатување на {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/mn/ghost.json b/ghost/i18n/locales/mn/ghost.json index 5722ba5b137..9dbb27a62c9 100644 --- a/ghost/i18n/locales/mn/ghost.json +++ b/ghost/i18n/locales/mn/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Баярлалаа!", "Become a paid member of {site} to get access to all premium content.": "{site}-н бүх премиум контентод хандах эрхтэй болохын тулд төлбөртэй гишүүн болно уу.", @@ -32,10 +36,12 @@ "Name": "Нэр", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "төлбөртэй", "Please confirm your email address with this link:": "Та өөрийн имэйл хаягаа дараах холбоосоор баталгаажуулна уу:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle}-д нэвтрэх нууцлал бүхий холбоос", "See you soon!": "Удахгүй уулзацгаая!", "Sent to {email}": "{email} хаяг руу илгээлээ", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Таны захиалга үүсэхгүй.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Таны {siteTitle} захиалга идэвжихэд ганцхан алхам үлдлээ - энэхүү холбоосоор өөрийн имэйлээ идэхжүүлнэ үү:", "You're one tap away from subscribing to {siteTitle}!": "Та {siteTitle}-д захиалга өгөхөд ганцхан алхам үлдлээ!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Таны үнэгүй туршилт {date}-нд дуусах бөгөөд энэ үед танаас үндсэн үнийг авна. Та үүнээс өмнө хэзээ ч цуцалж болно.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Таны захиалга цуцлагдсан бөгөөд {date}-нд дуусна. Та бүртгэлийн тохиргоогоороо захиалгаа үргэлжлүүлж болно.", "Your subscription has expired.": "Таны захиалга дууссан.", diff --git a/ghost/i18n/locales/ms/ghost.json b/ghost/i18n/locales/ms/ghost.json index 48959c87012..d58f64e9681 100644 --- a/ghost/i18n/locales/ms/ghost.json +++ b/ghost/i18n/locales/ms/ghost.json @@ -1,4 +1,6 @@ { + "{count} month_other": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Semoga berjaya!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +34,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Sila sahkan alamat e-mel anda dengan pautan ini:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Pautan log masuk untuk {siteTitle}", "See you soon!": "Jumpa lagi!", "Sent to {email}": "Dihantar ke {email}", @@ -76,6 +80,7 @@ "You will not be subscribed.": "Anda tidak akan dilanggan.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Anda hanya satu ketikan dari melanggan{siteTitle} — sila sahkan alamat e-mel anda dengan pautan ini:", "You're one tap away from subscribing to {siteTitle}!": "Anda hanya satu ketikan dari melanggan {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/nb/ghost.json b/ghost/i18n/locales/nb/ghost.json index cf110d2ae0e..b8c61e28645 100644 --- a/ghost/i18n/locales/nb/ghost.json +++ b/ghost/i18n/locales/nb/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Alt det beste!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "Navn", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "betalt", "Please confirm your email address with this link:": "Vennligst bekreft e-postadressen din med denne lenken:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Sikker påloggingslenke for {siteTitle}", "See you soon!": "Ser deg snart!", "Sent to {email}": "Sendt til {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Du vil ikke bli meldt på som abonnent.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Du er ett klikk unna å abonnere på {siteTitle} — bekreft e-postadressen din med denne lenken:", "You're one tap away from subscribing to {siteTitle}!": "Du er ett klikk unna å abonnere på {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Din gratis prøveperiode avsluttes den {date}, da blir du belastet med normal pris. Du kan alltid kansellere før den tid.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Abonnementet ditt er kansellert og utløper den {date}. Du kan gjenoppta abonnementet via kontoinnstillingene dine.", "Your subscription has expired.": "Abonnementet ditt er utløpt.", diff --git a/ghost/i18n/locales/ne/ghost.json b/ghost/i18n/locales/ne/ghost.json index dcae99d97d2..caf4697a7b8 100644 --- a/ghost/i18n/locales/ne/ghost.json +++ b/ghost/i18n/locales/ne/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "सबै राम्रो होस्!", "Become a paid member of {site} to get access to all premium content.": "सबै प्रिमियम सामग्रीमा पहुँच प्राप्त गर्न {site} को सशुल्क सदस्य बन्नुहोस्।", @@ -32,10 +36,12 @@ "Name": "नाम", "New comment on {postTitle}": "{postTitle} मा नयाँ प्रतिक्रिया", "New reply to your comment on {siteTitle}": "{siteTitle} मा तपाईंको प्रतिक्रियाको नयाँ प्रत्युत्तर", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "भुक्तान गरिएको", "Please confirm your email address with this link:": "कृपया यो लिङ्कसँग तपाईंको इमेल ठेगाना पुष्टि गर्नुहोस्:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} को लागि सुरक्षित साइन इन लिङ्क", "See you soon!": "चाँडै भेटौँला!", "Sent to {email}": "{email} मा पठाइयो", @@ -76,6 +82,7 @@ "You will not be subscribed.": "तपाईंलाई सदस्यता दिइने छैन।", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "तपाईं {siteTitle} को सदस्यता लिन एक ट्याप टाढा हुनुहुन्छ — कृपया यो लिङ्कसँग तपाईंको इमेल ठेगाना पुष्टि गर्नुहोस्:", "You're one tap away from subscribing to {siteTitle}!": "तपाईं {siteTitle} को सदस्यता लिन एक ट्याप टाढा हुनुहुन्छ!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "तपाईंको निःशुल्क परीक्षण {date} मा समाप्त हुन्छ, जुन समयमा तपाईंलाई नियमित मूल्यमा शुल्क लिइनेछ। तपाईं सधैं त्यसअघि रद्द गर्न सक्नुहुन्छ।", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "तपाईंको सदस्यता रद्द गरिएको छ र {date} मा म्याद सकिने छ। तपाईं आफ्नो खाता सेटिङ्हरू मार्फत सदस्यता पुनः सुरु गर्न सक्नुहुन्छ।", "Your subscription has expired.": "तपाईंको सदस्यताको म्याद सकिएको छ।", diff --git a/ghost/i18n/locales/nl/ghost.json b/ghost/i18n/locales/nl/ghost.json index ff4ba88c8cb..43e6d2c8222 100644 --- a/ghost/i18n/locales/nl/ghost.json +++ b/ghost/i18n/locales/nl/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Tot snel!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "Naam", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "betaald", "Please confirm your email address with this link:": "Bevestig je e-mailadres met deze link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Veilige inloglink voor {siteTitle}", "See you soon!": "Tot snel!", "Sent to {email}": "Verzonden naar {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Je zult niet worden geabonneerd.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Je bent één klik verwijderd van een abonnement op {siteTitle} — bevestig je e-mailadres met deze link:", "You're one tap away from subscribing to {siteTitle}!": "Je bent één klik verwijderd van een abonnement op {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Je gratis proefperiode eindigt op {date}, waarna de reguliere prijs in rekening wordt gebracht. Je kunt altijd voor die tijd annuleren.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Je abonnement is geannuleerd en verloopt op {date}. Je kunt je abonnement hervatten via je accountinstellingen.", "Your subscription has expired.": "Je abonnement is verlopen.", diff --git a/ghost/i18n/locales/nn/ghost.json b/ghost/i18n/locales/nn/ghost.json index 529aed8aeba..d65a1c229e9 100644 --- a/ghost/i18n/locales/nn/ghost.json +++ b/ghost/i18n/locales/nn/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Beste helsing!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Ver gild og bekreft e-postadressa di med denne lenka:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Trygg innlogginslenke for {siteTitle}", "See you soon!": "På gjensyn!", "Sent to {email}": "Sendt til {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Du vil ikkje bli ein abonnent.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Du er eitt klikk unna å abonnera på {siteTitle} – ver gild og bekreft e-postadressa di med denne lenka:", "You're one tap away from subscribing to {siteTitle}!": "Du er eitt klikk unna å abonnera på {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/pa/ghost.json b/ghost/i18n/locales/pa/ghost.json index cf05181d705..3dbcbe142ce 100644 --- a/ghost/i18n/locales/pa/ghost.json +++ b/ghost/i18n/locales/pa/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "ਸ਼ੁਭਕਾਮਨਾਵਾਂ!", "Become a paid member of {site} to get access to all premium content.": "{site} ਦੇ ਸਹਿਯੋਗੀ ਮੈਂਬਰ ਬਣ ਕੇ ਸਾਰੀ ਪ੍ਰੀਮੀਅਮ ਸਮੱਗਰੀ ਤੱਕ ਪਹੁੰਚ ਪ੍ਰਾਪਤ ਕਰੋ।", @@ -32,10 +36,12 @@ "Name": "ਨਾਮ", "New comment on {postTitle}": "{postTitle} 'ਤੇ ਨਵੀਂ ਟਿੱਪਣੀ", "New reply to your comment on {siteTitle}": "{siteTitle} 'ਤੇ ਤੁਹਾਡੀ ਟਿੱਪਣੀ 'ਤੇ ਨਵਾਂ ਜਵਾਬ", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "ਭੁਗਤਾਨ ਕੀਤਾ", "Please confirm your email address with this link:": "ਕਿਰਪਾ ਕਰਕੇ ਇਸ ਲਿੰਕ ਨਾਲ ਆਪਣੇ ਈਮੇਲ ਪਤੇ ਦੀ ਪੁਸ਼ਟੀ ਕਰੋ:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} ਲਈ ਸੁਰੱਖਿਅਤ ਸਾਈਨ-ਇਨ ਲਿੰਕ", "See you soon!": "ਜਲਦੀ ਮਿਲਦੇ ਹਾਂ!", "Sent to {email}": "{email} ਨੂੰ ਭੇਜਿਆ ਗਿਆ", @@ -76,6 +82,7 @@ "You will not be subscribed.": "ਤੁਹਾਨੂੰ ਸਬਸਕ੍ਰਾਈਬ ਨਹੀਂ ਕੀਤਾ ਜਾਵੇਗਾ।", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "ਤੁਸੀਂ {siteTitle} ਤੇ ਸਬਸਕ੍ਰਾਈਬ ਕਰਨ ਦੇ ਬਹੁਤ ਨੇੜੇ ਹੋ — ਕਿਰਪਾ ਕਰਕੇ ਇਸ ਲਿੰਕ ਨਾਲ ਆਪਣੇ ਈਮੇਲ ਪਤੇ ਦੀ ਪੁਸ਼ਟੀ ਕਰੋ:", "You're one tap away from subscribing to {siteTitle}!": "ਤੁਸੀਂ {siteTitle} ਤੇ ਸਬਸਕ੍ਰਾਈਬ ਕਰਨ ਦੇ ਬਹੁਤ ਨੇੜੇ ਹੋ!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "ਤੁਹਾਡੀ ਅਜ਼ਮਾਇਸ਼ੀ ਸਦੱਸਤਾ {date} ਨੂੰ ਸਮਾਪਤ ਹੋਵੇਗੀ, ਉਸ ਸਮੇਂ ਤੁਹਾਡੇ ਤੋਂ ਨਿਯਮਤ ਕੀਮਤ ਲਈ ਜਾਵੇਗੀ। ਤੁਸੀਂ ਹਮੇਸ਼ਾ ਇਸ ਤਾਰੀਕ ਤੋਂ ਪਹਿਲਾਂ ਰੱਦ ਕਰ ਸਕਦੇ ਹੋ।", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "ਤੁਹਾਡੀ ਸਦੱਸਤਾ ਰੱਦ ਕਰ ਦਿੱਤੀ ਗਈ ਹੈ ਅਤੇ {date} ਨੂੰ ਮਿਆਦ ਪੁੱਗ ਜਾਵੇਗੀ। ਤੁਸੀਂ ਆਪਣੀਆਂ ਖਾਤਾ ਸੈਟਿੰਗਾਂ ਰਾਹੀਂ ਆਪਣੀ ਸਦੱਸਤਾ ਮੁੜ ਸ਼ੁਰੂ ਕਰ ਸਕਦੇ ਹੋ।", "Your subscription has expired.": "ਤੁਹਾਡੀ ਸਦੱਸਤਾ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ।", diff --git a/ghost/i18n/locales/pl/ghost.json b/ghost/i18n/locales/pl/ghost.json index b31fa899b65..f8256ed08fb 100644 --- a/ghost/i18n/locales/pl/ghost.json +++ b/ghost/i18n/locales/pl/ghost.json @@ -1,4 +1,12 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Wszystkiego dobrego!", "Become a paid member of {site} to get access to all premium content.": "Zostań płatnym członkiem {site}, aby uzyskać dostęp do wszystkich treści premium.", @@ -32,10 +40,12 @@ "Name": "Imię", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "płatnym", "Please confirm your email address with this link:": "Potwierdź proszę swój adres email, klikając w ten link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Bezpieczny link logowania do {siteTitle}", "See you soon!": "Do zobaczenia!", "Sent to {email}": "Wysłano do {email}", @@ -76,6 +86,7 @@ "You will not be subscribed.": "Nie zostaniesz zapisany.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Jesteś jedno kliknięcie od zapisania się do {siteTitle} - potwierdź proszę adres email tym linkiem:", "You're one tap away from subscribing to {siteTitle}!": "Jesteś jedno kliknięcie od zapisania się do {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Twój bezpłatny okres próbny kończy się {date}, w którym to czasie zostaniesz obciążony standardową ceną. Zawsze możesz anulować przed tym terminem.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Twoja subskrypcja została anulowana i wygaśnie w dniu {date}. Możesz wznowić subskrypcję w ustawieniach swojego konta.", "Your subscription has expired.": "Twoja subskrypcja wygasła", diff --git a/ghost/i18n/locales/pt-BR/ghost.json b/ghost/i18n/locales/pt-BR/ghost.json index 3d7e3abb5df..760ec3f6b3a 100644 --- a/ghost/i18n/locales/pt-BR/ghost.json +++ b/ghost/i18n/locales/pt-BR/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Até breve!", "Become a paid member of {site} to get access to all premium content.": "Torne-se um membro pago do {site} para ter acesso a todo o conteúdo premium.", @@ -32,10 +38,12 @@ "Name": "Nome", "New comment on {postTitle}": "Novo comentário em {postTitle}", "New reply to your comment on {siteTitle}": "Nova resposta ao seu comentário em {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Ou use este link para acessar com segurança", "Or, skip the code and sign in directly": "Ou, ignore o código e entre diretamente", "paid": "pago", "Please confirm your email address with this link:": "Por favor, confirme seu endereço de e-mail usando este link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Link seguro para acessar o site {siteTitle}", "See you soon!": "Até logo!", "Sent to {email}": "Enviado para {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Você não será inscrito.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Você está a um clique de se inscrever no site {siteTitle} — por favor, confirme seu endereço de e-mail com este link:", "You're one tap away from subscribing to {siteTitle}!": "Você está a um clique de se inscrever no site {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Seu período de teste gratuito termina em {date}, quando será cobrado o preço normal. Você pode cancelar antes disso.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Sua assinatura foi cancelada e expirará em {date}. Você pode retomar sua assinatura nas configurações de sua conta.", "Your subscription has expired.": "Sua assinatura expirou.", diff --git a/ghost/i18n/locales/pt/ghost.json b/ghost/i18n/locales/pt/ghost.json index 4a904108d07..13c81014a31 100644 --- a/ghost/i18n/locales/pt/ghost.json +++ b/ghost/i18n/locales/pt/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Tudo de bom para si!", "Become a paid member of {site} to get access to all premium content.": "Torne-se um membro pago de {site} para aceder a todo o conteúdo exclusivo.", @@ -32,10 +38,12 @@ "Name": "Nome", "New comment on {postTitle}": "Novo comentário em {postTitle}", "New reply to your comment on {siteTitle}": "Nova resposta ao seu comentário em {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "paga", "Please confirm your email address with this link:": "Por favor, confirme o seu email através deste link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Ligação segura para entrar em {siteTitle}", "See you soon!": "Até breve!", "Sent to {email}": "Enviado para {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Não irá ficar subscrito.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Está a um passo de subscrever o {siteTitle} - Por favor, confirme o seu endereço de email com este link:", "You're one tap away from subscribing to {siteTitle}!": "Está a um passo de subscrever o {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "A sua avaliação gratuita termina a {date}, quando será cobrado preço normal. Pode sempre cancelar antes disso.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "A sua subscrição foi cancelada e irá expirar a {date}. Pode recomeçar a sua subscrição nas definições da sua conta.", "Your subscription has expired.": "A sua subscrição expirou.", diff --git a/ghost/i18n/locales/ro/ghost.json b/ghost/i18n/locales/ro/ghost.json index f11c92cb128..e0e80c94734 100644 --- a/ghost/i18n/locales/ro/ghost.json +++ b/ghost/i18n/locales/ro/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Toate cele bune!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +38,12 @@ "Name": "Nume", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "plătit", "Please confirm your email address with this link:": "Te rugăm să confirmi adresa ta de email cu acest link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Link securizat pentru autentificarea la {siteTitle}", "See you soon!": "Pe curând!", "Sent to {email}": "Trimis către {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Nu vei fi abonat.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Ești la un pas de a te abona la {siteTitle} — te rugăm să confirmi adresa ta de email cu acest link:", "You're one tap away from subscribing to {siteTitle}!": "Ești la un singur pas de a te abona la {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Perioada ta de probă gratuită se încheie pe {date}, moment în care vei fi taxat la prețul obișnuit. Poți anula oricând până atunci.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Abonamentul tău a fost anulat și va expira pe {date}. Poți relua abonamentul din setările contului.", "Your subscription has expired.": "Abonamentul tău a expirat.", diff --git a/ghost/i18n/locales/ru/ghost.json b/ghost/i18n/locales/ru/ghost.json index d048a06294b..cca92484bee 100644 --- a/ghost/i18n/locales/ru/ghost.json +++ b/ghost/i18n/locales/ru/ghost.json @@ -1,4 +1,12 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Всего наилучшего!", "Become a paid member of {site} to get access to all premium content.": "Станьте платным участником {site}, чтобы получить доступ ко всему премиум-контенту.", @@ -32,10 +40,12 @@ "Name": "Имя", "New comment on {postTitle}": "Новый комментарий к записи {postTitle}", "New reply to your comment on {siteTitle}": "Новый ответ на ваш комментарий на сайте {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Или воспользуйтесь этой ссылкой для безопасного входа", "Or, skip the code and sign in directly": "Или пропустите ввод кода и войдите напрямую", "paid": "платным", "Please confirm your email address with this link:": "Пожалуйста, подтвердите свой email адрес, перейдя по этой ссылке:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Ссылка для безопасного входа в систему на {siteTitle}", "See you soon!": "До скорой встречи!", "Sent to {email}": "Отправлено на {email}", @@ -76,6 +86,7 @@ "You will not be subscribed.": "Вы не будете подписаны.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Вы в одном клике от подписки на {siteTitle} — пожалуйста, подтвердите свой email адрес с помощью этой ссылки:", "You're one tap away from subscribing to {siteTitle}!": "Вы в одном клике от подписки на {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Ваш бесплатный пробный период заканчивается {date}, после чего с вас будет списана стандартная сумма. Вы всегда можете отменить подписку до этого момента.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Ваша подписка была отменена и истечёт {date}. Вы можете возобновить подписку в настройках своего аккаунта.", "Your subscription has expired.": "Ваша подписка истекла.", diff --git a/ghost/i18n/locales/si/ghost.json b/ghost/i18n/locales/si/ghost.json index e696519a9df..17359ac7d52 100644 --- a/ghost/i18n/locales/si/ghost.json +++ b/ghost/i18n/locales/si/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "සියලු දේ සාර්ථකත්වයට පත් වේවායි ප්\u200dරාර්ථනා කරන්නෙමු!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "ඔබගේ email ලිපිනය තහවුරු කිරීම සඳහා මෙම link එක භාවිතා කරන්න:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} වෙත ආරක්ෂිතව sign in වීම සඳහා වන link එක", "See you soon!": "ඉක්මණින් නැවත හමුවෙමු!", "Sent to {email}": "{email} වෙත යවන ලදී", @@ -76,6 +82,7 @@ "You will not be subscribed.": "ඔබව subscribe නොවනු ඇත.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "{siteTitle} වෙත subscribe වීම සඳහා ඇත්තේ තව එක පියවරක් පමණි - කරුණාකර පහත link එක සමඟින් ඔබගේ email ලිපිනය තහවුරු කරන්න:", "You're one tap away from subscribing to {siteTitle}!": "{siteTitle} වෙත subscribe වීම සඳහා ඇත්තේ තව එක පියවරක් පමණි!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/sk/ghost.json b/ghost/i18n/locales/sk/ghost.json index 16a81f91a47..b01917f51ea 100644 --- a/ghost/i18n/locales/sk/ghost.json +++ b/ghost/i18n/locales/sk/ghost.json @@ -1,4 +1,12 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Všetko dobré!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +40,12 @@ "Name": "Meno", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "plateným", "Please confirm your email address with this link:": "Prosím potvrďte svoju e-mailovú adresu kliknutím na nasledujúci odkaz", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Bezpečný prihlasovací odkaz pre {siteTitle}", "See you soon!": "Dovidenia!", "Sent to {email}": "Odoslané na {email}", @@ -76,6 +86,7 @@ "You will not be subscribed.": "Nebudete prihlásený k odberu.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Ste jedno kliknutie od odberu z {siteTitle} — prosím potvrďte svoju e-mailovú adresu pomocou nasledujúceho odkazu:", "You're one tap away from subscribing to {siteTitle}!": "Ste jedno kliknutie od odberu z {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Vaše bezplatné skúšobné obdobie skončí {date} a v tom čase vám bude účtovaný poplatok za predplatné. Môžete to pred tým zrušiť.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Vaše predplatné bolo zrušené a skončí {date}. Svoje predplatné môžete obnoviť v nastaveniach svojho účtu.", "Your subscription has expired.": "Vaše predplatné vypršalo", diff --git a/ghost/i18n/locales/sl/ghost.json b/ghost/i18n/locales/sl/ghost.json index b8220bf7676..10515e5498c 100644 --- a/ghost/i18n/locales/sl/ghost.json +++ b/ghost/i18n/locales/sl/ghost.json @@ -1,4 +1,12 @@ { + "{count} month_one": "", + "{count} month_two": "", + "{count} month_few": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_two": "", + "{count} year_few": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Vse dobro!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +40,12 @@ "Name": "Ime", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "plačljivi", "Please confirm your email address with this link:": "S to povezavo potrdite svoj e-poštni naslov:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Varna povezava za prijavo na {siteTitle}", "See you soon!": "Se vidimo kmalu!", "Sent to {email}": "Poslano na {email}", @@ -76,6 +86,7 @@ "You will not be subscribed.": "Ne boste naročeni.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Le en klik vas loči od tega, da se naročite na {siteTitle} - potrdite svoj e-poštni naslov s to povezavo:", "You're one tap away from subscribing to {siteTitle}!": "Le en klik vas loči od tega, da se naročite na {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Vaše brezplačno poskusno obdobje se konča {date}, nato vam bo zaračunana redna cena. Naročnino lahko vedno prekličete pred tem datumom.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Vaša naročnina je bila preklicana in bo potekla {date}. Naročnino lahko obnovite prek nastavitev svojega računa.", "Your subscription has expired.": "Vaša naročnina je potekla.", diff --git a/ghost/i18n/locales/sq/ghost.json b/ghost/i18n/locales/sq/ghost.json index 1608022e64b..cfb554f068b 100644 --- a/ghost/i18n/locales/sq/ghost.json +++ b/ghost/i18n/locales/sq/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Gjithe te mirat!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Ju lutem konfirmoni adresen tuaj te emailit me kete link:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Lidhja e sigurt e hyrjes për {siteTitle}", "See you soon!": "Shihemi se shpejti!", "Sent to {email}": "Derguar tek {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Ju nuk do te abonoheni", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Ju jeni nje klikim larg nga abonimi ne {siteTitle}! - ju lutem konfirmoni adresen tuaj te emailit me kete link.", "You're one tap away from subscribing to {siteTitle}!": "Ju jeni nje klikim larg nga abonimi ne {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/sr-Cyrl/ghost.json b/ghost/i18n/locales/sr-Cyrl/ghost.json index a5ff1ba1bdd..400dd3f7f05 100644 --- a/ghost/i18n/locales/sr-Cyrl/ghost.json +++ b/ghost/i18n/locales/sr-Cyrl/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Све најбоље!", "Become a paid member of {site} to get access to all premium content.": "Постаните плаћени члан {site}-а да бисте добили приступ премијум садржају.", @@ -32,10 +38,12 @@ "Name": "Име", "New comment on {postTitle}": "Нови коментар на {postTitle}", "New reply to your comment on {siteTitle}": "Нови одговор на Ваш коментар на {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Или искористите овај линк да се сигурно пријавите", "Or, skip the code and sign in directly": "Или, прескочите код и пријавите се директно", "paid": "плаћени", "Please confirm your email address with this link:": "Молимо Вас да потврдите Вашу email адресу путем овог линка:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Сигуран линк за пријаву на {siteTitle}", "See you soon!": "Видимо се ускоро!", "Sent to {email}": "Послато на {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Нећете бити претплаћени.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Један сте клик од претплате на {siteTitle} - молимо Вас да потврдите Вашу email адресу користећи овај линк:", "You're one tap away from subscribing to {siteTitle}!": "Један сте клик од претплате на {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Ваш бесплатан пробни период истиче {date}, када ће Вам бити наплаћена редовна цена. Увек можете отказати пре тог рока.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Ваша претплата је отказана и истиче {date}. Можете наставити претплату у подешавањима Вашег профила.", "Your subscription has expired.": "Ваша претплата је истекла.", diff --git a/ghost/i18n/locales/sr/ghost.json b/ghost/i18n/locales/sr/ghost.json index e806ea25276..8f82683f09f 100644 --- a/ghost/i18n/locales/sr/ghost.json +++ b/ghost/i18n/locales/sr/ghost.json @@ -1,4 +1,10 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Sve najbolje!", "Become a paid member of {site} to get access to all premium content.": "Postanite plaćeni član {site}-a da biste dobili pristup premijum sadržaju.", @@ -32,10 +38,12 @@ "Name": "Ime", "New comment on {postTitle}": "Novi komentar na {postTitle}", "New reply to your comment on {siteTitle}": "Novi odgovor na Vaš komentar na {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Ili iskoristite ovaj link da se sigurno prijavite", "Or, skip the code and sign in directly": "Ili, preskočite kod i prijavite se direktno", "paid": "plaćeni", "Please confirm your email address with this link:": "Molimo Vas da potvrdite Vašu email adresu putem ovog linka:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Siguran link za prijavu na {siteTitle}", "See you soon!": "Vidimo se uskoro!", "Sent to {email}": "Poslato na {email}", @@ -76,6 +84,7 @@ "You will not be subscribed.": "Nećete biti pretplaćeni.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Jedan ste klik od pretplate na {siteTitle} - molimo Vas da potvrdite Vašu email adresu koristeći ovaj link:", "You're one tap away from subscribing to {siteTitle}!": "Jedan ste klik od pretplate na {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Vaš besplatan probni period ističe {date}, kada će Vam biti naplaćena redovna cena. Uvek možete otkazati pre tog roka.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Vaša pretplata je otkazana i ističe {date}. Možete nastaviti pretplatu u podešavanjima Vašeg profila.", "Your subscription has expired.": "Vaša pretplata je istekla.", diff --git a/ghost/i18n/locales/sv/ghost.json b/ghost/i18n/locales/sv/ghost.json index ba07f65dee7..06106d39299 100644 --- a/ghost/i18n/locales/sv/ghost.json +++ b/ghost/i18n/locales/sv/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Allt gott!", "Become a paid member of {site} to get access to all premium content.": "Bli betalande medlem på {site} för att få tillgång till allt innehåll.", @@ -32,10 +36,12 @@ "Name": "Namn", "New comment on {postTitle}": "Ny kommentar i {postTitle}", "New reply to your comment on {siteTitle}": "Nytt svar till din kommentar på {siteTitle}", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Eller använd denna länk för att logga in säkert", "Or, skip the code and sign in directly": "Eller hoppa över koden och logga in direkt", "paid": "betalande", "Please confirm your email address with this link:": "Vänligen bekräfta din e-postadress med denna länk:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Säker inloggningslänk för {siteTitle}", "See you soon!": "Vi ses snart!", "Sent to {email}": "Skickat till {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Du kommer inte att prenumerera.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Du är ett klick bort från att prenumerera på {siteTitle} — vänligen bekräfta din e-postadress med denna länk:", "You're one tap away from subscribing to {siteTitle}!": "Du är ett klick bort från att prenumerera på {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Din kostnadsfria provperiod slutar den {date}, efter vilket du kommer bli debiterad det vanliga priset. Du kan alltid avbryta innan dess.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Din prenumeration har avbrutits och kommer löpa ut den {date}. Du kan reaktivera din prenumeration via dina kontoinställningar.", "Your subscription has expired.": "Din prenumeration har löpt ut.", diff --git a/ghost/i18n/locales/sw/ghost.json b/ghost/i18n/locales/sw/ghost.json index 463a18d6adf..5d597cf8418 100644 --- a/ghost/i18n/locales/sw/ghost.json +++ b/ghost/i18n/locales/sw/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Kila la kheri!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Tafadhali thibitisha anwani yako ya barua pepe na kiungo hiki:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Kiungo salama cha kuingia kwa {siteTitle}", "See you soon!": "Tutaonana hivi karibuni!", "Sent to {email}": "Imetumwa kwa {email}", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Hutajiandikisha.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Uko umbali wa mguso mmoja tu kujiunga na {siteTitle} — tafadhali thibitisha anwani yako ya barua pepe na kiungo hiki:", "You're one tap away from subscribing to {siteTitle}!": "Uko umbali wa mguso mmoja tu kujiunga na {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/ta/ghost.json b/ghost/i18n/locales/ta/ghost.json index aed73a8000d..7f022586bd7 100644 --- a/ghost/i18n/locales/ta/ghost.json +++ b/ghost/i18n/locales/ta/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "அனைத்து நல்வாழ்த்துக்களும்!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "இந்த இணைப்புடன் உங்கள் மின்னஞ்சல் முகவரியை உறுதிப்படுத்தவும்:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} க்கான பாதுகாப்பான உள்நுழைவு இணைப்பு", "See you soon!": "உங்களை விரைவில் சந்திக்கிறோம்!", "Sent to {email}": "{email} க்கு அனுப்பப்பட்டது", @@ -76,6 +82,7 @@ "You will not be subscribed.": "நீங்கள் சந்தா செய்யப்படமாட்டீர்கள்.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "{siteTitle} க்கு சந்தா செய்ய உங்கள் மின்னஞ்சல் முகவரியை இந்த இணைப்புடன் உறுதிப்படுத்தவும்:", "You're one tap away from subscribing to {siteTitle}!": "{siteTitle} க்கு சந்தா செய்ய ஒரு தட்டல் மட்டுமே தூரத்தில் உள்ளீர்கள்!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/th/ghost.json b/ghost/i18n/locales/th/ghost.json index 71f35f6f283..d1708be72b0 100644 --- a/ghost/i18n/locales/th/ghost.json +++ b/ghost/i18n/locales/th/ghost.json @@ -1,4 +1,6 @@ { + "{count} month_other": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "ขอให้โชคดี!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +34,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "โปรดยืนยันที่อยู่อีเมลของคุณด้วยลิงก์นี้:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "ลิงก์ลงชื่อเข้าใช้อย่างปลอดภัยสำหรับ {siteTitle}", "See you soon!": "แล้วพบกัน!", "Sent to {email}": "ส่งไปที่ {email}", @@ -76,6 +80,7 @@ "You will not be subscribed.": "คุณจะไม่ถูกรับสมัครข้อมูล", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "เพียงแตะครั้งเดียวคุณก็จะสมัครรับข้อมูลจาก {siteTitle} — โปรดยืนยันที่อยู่อีเมลของคุณด้วยลิงก์นี้:", "You're one tap away from subscribing to {siteTitle}!": "เพียงแตะครั้งเดียวคุณก็จะสมัครรับข้อมูลจาก {siteTitle} แล้ว!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/tr/ghost.json b/ghost/i18n/locales/tr/ghost.json index 43ff2ee5207..3c148a39535 100644 --- a/ghost/i18n/locales/tr/ghost.json +++ b/ghost/i18n/locales/tr/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "En iyi dileklerimizle!", "Become a paid member of {site} to get access to all premium content.": "Tüm premium içeriklere erişebilmek için {site} ücretli üyesi olun.", @@ -32,10 +36,12 @@ "Name": "İsim", "New comment on {postTitle}": "{postTitle} için yeni yorum", "New reply to your comment on {siteTitle}": "{siteTitle} sitesindeki yorumunuza yeni cevap", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Ya da güvenli giriş yapmak için bu bağlantıyı kullan", "Or, skip the code and sign in directly": "Ya da kodu atlayıp doğrudan oturum aç", "paid": "ücretli", "Please confirm your email address with this link:": "Lütfen e-posta adresini bu bağlantıya tıklayarak onayla:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} için güvenli giriş bağlantısı", "See you soon!": "Yakında görüşmek üzere!", "Sent to {email}": "{email} adresine gönderildi", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Abone olmayacaksın.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "{siteTitle} sitesine abone olmaya bir tık uzaktasın — lütfen e-posta adresini bu bağlantıya tıklayarak onayla:", "You're one tap away from subscribing to {siteTitle}!": "{siteTitle} abone olmanıza bir dokunuş kadar yakınsınız!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Ücretsiz denemeniz {date} tarihinde sona eriyor, bu tarihten sonra düzenli fiyat uygulanacaktır. O zamana kadar her zaman iptal edebilirsiniz.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Aboneliğiniz iptal edilmiştir ve {date} tarihinde sona erecektir. Aboneliğinizi hesap ayarları üzerinden tekrar başlatabilirsiniz.", "Your subscription has expired.": "Aboneliğiniz sona erdi.", diff --git a/ghost/i18n/locales/uk/ghost.json b/ghost/i18n/locales/uk/ghost.json index 34a41a271b4..96ecac95b8d 100644 --- a/ghost/i18n/locales/uk/ghost.json +++ b/ghost/i18n/locales/uk/ghost.json @@ -1,4 +1,12 @@ { + "{count} month_one": "", + "{count} month_few": "", + "{count} month_many": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_few": "", + "{count} year_many": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Всього найкращого!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +40,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Будь ласка, підтвердь свою електронну пошту за допомогою цього посилання:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Безпечне посилання для входу до {siteTitle}", "See you soon!": "До зустрічі!", "Sent to {email}": "Відправлено на {email}", @@ -76,6 +86,7 @@ "You will not be subscribed.": "Тебе не буде підписано.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Ви за один крок до підписки на {siteTitle} — будь ласка, підтвердь свою електронну пошту за допомогою цього посилання:", "You're one tap away from subscribing to {siteTitle}!": "Ви за один крок до підписки на {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/ur/ghost.json b/ghost/i18n/locales/ur/ghost.json index 7e6d7b91e31..94173d72bc2 100644 --- a/ghost/i18n/locales/ur/ghost.json +++ b/ghost/i18n/locales/ur/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "سب سے بہترین خواہشات!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "براہ کرم اس لنک کے ساتھ آپ کا ای میل پتہ تصدیق کریں:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} کے لئے محفوظ سائن ان لنک", "See you soon!": "جلد ہی ملیں گے!", "Sent to {email}": "{email} کو بھیجا گیا", @@ -76,6 +82,7 @@ "You will not be subscribed.": "آپ کو سبسکرائب نہیں کیا جائے گا۔", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "{siteTitle} کو سبسکرائب ہونے کے لئے آپ ایک ٹیپ کے فاصلے پر ہیں — براہ کرم اس لنک کے ساتھ آپ کا ای میل پتہ تصدیق کریں:", "You're one tap away from subscribing to {siteTitle}!": "{siteTitle} کو سبسکرائب ہونے کے لئے آپ ایک ٹیپ کے فاصلے پر ہیں!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/uz/ghost.json b/ghost/i18n/locales/uz/ghost.json index 872c11ce49d..5b1efa68c1c 100644 --- a/ghost/i18n/locales/uz/ghost.json +++ b/ghost/i18n/locales/uz/ghost.json @@ -1,4 +1,8 @@ { + "{count} month_one": "", + "{count} month_other": "", + "{count} year_one": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Barcha ezgu tilaklar!", "Become a paid member of {site} to get access to all premium content.": "", @@ -32,10 +36,12 @@ "Name": "", "New comment on {postTitle}": "", "New reply to your comment on {siteTitle}": "", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "", "Or, skip the code and sign in directly": "", "paid": "", "Please confirm your email address with this link:": "Iltimos, elektron pochta manzilingizni ushbu havola bilan tasdiqlang:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "{siteTitle} uchun xavfsiz kirish havolasi", "See you soon!": "Ko'rishguncha!", "Sent to {email}": "{email}ga yuborildi", @@ -76,6 +82,7 @@ "You will not be subscribed.": "Siz obuna bo'lmaysiz.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "{siteTitle} ga obuna boʻlish uchun bir qadam qoldi — elektron pochta manzilingizni ushbu havola orqali tasdiqlang:", "You're one tap away from subscribing to {siteTitle}!": "", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "", "Your subscription has expired.": "", diff --git a/ghost/i18n/locales/vi/ghost.json b/ghost/i18n/locales/vi/ghost.json index 39bbee3c910..25c1082acf1 100644 --- a/ghost/i18n/locales/vi/ghost.json +++ b/ghost/i18n/locales/vi/ghost.json @@ -1,4 +1,6 @@ { + "{count} month_other": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "Chúc bạn mọi điều tốt đẹp nhất!", "Become a paid member of {site} to get access to all premium content.": "Trở thành thành viên trả phí của {site} để truy cập toàn bộ nội dung cao cấp.", @@ -32,10 +34,12 @@ "Name": "Tên", "New comment on {postTitle}": "Bài viết {postTitle} có bình luận mới", "New reply to your comment on {siteTitle}": "Bình luận của bạn trên {siteTitle} có phản hồi mới", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "Hoặc dùng liên kết này để đăng nhập an toàn", "Or, skip the code and sign in directly": "Hoặc, bỏ qua mã và đăng nhập trực tiếp", "paid": "trả phí", "Please confirm your email address with this link:": "Vui lòng xác nhận địa chỉ email của bạn bằng liên kết này:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "Liên kết đăng nhập {siteTitle} an toàn", "See you soon!": "Hẹn gặp lại bạn!", "Sent to {email}": "Đã gửi đến {email}", @@ -76,6 +80,7 @@ "You will not be subscribed.": "Bạn sẽ không đăng ký gói.", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Bạn còn một bước nữa để đăng ký {siteTitle} — vui lòng xác nhận địa chỉ email của bạn bằng liên kết này:", "You're one tap away from subscribing to {siteTitle}!": "Bạn còn một bước nữa để đăng ký {siteTitle}!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Gói đọc thử của bạn sẽ kết thúc vào {date} và sau đó bắt đầu tính phí. Bạn luôn có thể hủy gói trước thời điểm này.", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Gói thành viên của bạn đã bị hủy và sẽ hết hạn vào {date}. Bạn có thể tiếp tục lại trong cài đặt tài khoản.", "Your subscription has expired.": "Gói thành viên của bạn đã hết hạn.", diff --git a/ghost/i18n/locales/zh-Hant/ghost.json b/ghost/i18n/locales/zh-Hant/ghost.json index 7c48187cd86..dadebcbb906 100644 --- a/ghost/i18n/locales/zh-Hant/ghost.json +++ b/ghost/i18n/locales/zh-Hant/ghost.json @@ -1,4 +1,6 @@ { + "{count} month_other": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "一切順利", "Become a paid member of {site} to get access to all premium content.": "成為{site}的付費會員,即可暢享所有深度內容。", @@ -32,10 +34,12 @@ "Name": "名字", "New comment on {postTitle}": "{postTitle} 有新留言", "New reply to your comment on {siteTitle}": "對您在{siteTitle}上的評論的新回覆", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "或使用此連結安全登入", "Or, skip the code and sign in directly": "或者,跳過驗證碼直接登入", "paid": "已付款", "Please confirm your email address with this link:": "請透過此連結確認您的電子郵件地址:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "為{siteTitle}準備的安全登入連結", "See you soon!": "再見!", "Sent to {email}": "已發送到 {email}", @@ -76,6 +80,7 @@ "You will not be subscribed.": "系統不會為您訂閱任何內容。", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "您離訂閱{siteTitle}只差一步 — 請透過此連結確認您的電子郵件地址:", "You're one tap away from subscribing to {siteTitle}!": "點擊後即可完成{siteTitle}的訂閱!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "您的免費試用將會在{date}結束,結束後您將會被收取標準價格。您可以在試用期結束前取消。", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "您的訂閱已經取消,並且會在{date}後過期。您可以在會員設定中重新訂閱。", "Your subscription has expired.": "您的訂閱已經到期。", diff --git a/ghost/i18n/locales/zh/ghost.json b/ghost/i18n/locales/zh/ghost.json index aa8c07a119f..0e728b11ac6 100644 --- a/ghost/i18n/locales/zh/ghost.json +++ b/ghost/i18n/locales/zh/ghost.json @@ -1,4 +1,6 @@ { + "{count} month_other": "", + "{count} year_other": "", "{date}": "{date}", "All the best!": "一切顺利!", "Become a paid member of {site} to get access to all premium content.": "成为{site}的付费会员,即可畅享所有付费内容。", @@ -32,10 +34,12 @@ "Name": "姓名", "New comment on {postTitle}": "{postTitle}的新评论", "New reply to your comment on {siteTitle}": "您在{siteTitle}的评论收到新回复", + "Open this link to redeem your gift.": "", "Or use this link to securely sign in": "或通过此链接安全登录", "Or, skip the code and sign in directly": "或跳过验证码直接登录", "paid": "付费", "Please confirm your email address with this link:": "请通过此链接确认您的电子邮件地址:", + "Redeem your gift subscription": "", "Secure sign in link for {siteTitle}": "为 {siteTitle} 准备的安全登录链接", "See you soon!": "回见!", "Sent to {email}": "已发送至 {email}", @@ -76,6 +80,7 @@ "You will not be subscribed.": "您不会被订阅", "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "您离订阅 {siteTitle} 仅需点击一下 — 请通过此链接确认您的电子邮件地址:", "You're one tap away from subscribing to {siteTitle}!": "仅需一次点击即可完成{siteTitle}的订阅!", + "You've been gifted {cadenceLabel} of {siteTitle}": "", "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "您的免费试用将于{date}结束,届时将按标准价格收费。您可在到期前随时取消订阅。", "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "您的订阅已取消,将于{date}到期。您可通过账户设置重新订阅。", "Your subscription has expired.": "您的订阅已过期。",