0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added support for i18n in comments-ui (#17797)

refs https://github.com/TryGhost/Product/issues/3504

- This adds support for translations, but doesn't yet translate every
possible string in the app.
- Only active if beta translations is enabled
This commit is contained in:
Simon Backx 2023-08-23 15:57:37 +02:00 committed by GitHub
parent 9aa3b7e46f
commit 375a6d37c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 241 additions and 24 deletions

View file

@ -76,6 +76,7 @@
"vite": "4.4.9",
"vite-plugin-css-injected-by-js": "3.3.0",
"vite-plugin-svgr": "3.2.0",
"vitest": "0.34.2"
"vitest": "0.34.2",
"@tryghost/i18n": "0.0.0"
}
}

View file

@ -1,11 +1,12 @@
import AuthFrame from './AuthFrame';
import ContentBox from './components/ContentBox';
import PopupBox from './components/PopupBox';
import React, {useCallback, useEffect, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import i18nLib from '@tryghost/i18n';
import setupGhostApi from './utils/api';
import {ActionHandler, SyncActionHandler, isSyncAction} from './actions';
import {AdminApi, setupAdminAPI} from './utils/adminApi';
import {AppContext, AppContextType, CommentsOptions, DispatchActionType, EditableAppContext} from './AppContext';
import {AppContext, DispatchActionType, EditableAppContext} from './AppContext';
import {CommentsFrame} from './components/Frame';
import {useOptions} from './utils/options';
@ -13,14 +14,6 @@ type AppProps = {
scriptTag: HTMLElement;
};
function createContext(options: CommentsOptions, state: EditableAppContext): AppContextType {
return {
...options,
...state,
dispatchAction: (() => {}) as DispatchActionType
};
}
const App: React.FC<AppProps> = ({scriptTag}) => {
const options = useOptions(scriptTag);
const [state, setFullState] = useState<EditableAppContext>({
@ -44,8 +37,6 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
const [adminApi, setAdminApi] = useState<AdminApi|null>(null);
const context = createContext(options, state)
const setState = useCallback((newState: Partial<EditableAppContext> | ((state: EditableAppContext) => Partial<EditableAppContext>)) => {
setFullState((state) => {
if (typeof newState === 'function') {
@ -81,7 +72,17 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
return {};
});
}, [api, adminApi, options]); // Do not add state or context as a dependency here -> infinite render loop
context.dispatchAction = dispatchAction as DispatchActionType;
const i18n = useMemo(() => {
return i18nLib(options.locale, 'comments');
}, [options.locale]);
const context = {
...options,
...state,
t: i18n.t,
dispatchAction: dispatchAction as DispatchActionType
};
const initAdminAuth = async () => {
if (adminApi || !options.adminUrl) {

View file

@ -34,6 +34,7 @@ export type AddComment = {
}
export type CommentsOptions = {
locale: string,
siteUrl: string,
apiKey: string | undefined,
apiUrl: string | undefined,
@ -67,7 +68,8 @@ export type EditableAppContext = {
export type AppContextType = EditableAppContext & CommentsOptions & {
// This part makes sure we can add automatic data and return types to the actions when using context.dispatchAction('actionName', data)
// eslint-disable-next-line @typescript-eslint/ban-types
dispatchAction: <T extends ActionType | SyncActionType>(action: T, data: Parameters<(typeof Actions & typeof SyncActions)[T]>[0] extends {data: any} ? Parameters<(typeof Actions & typeof SyncActions)[T]>[0]['data'] : {}) => T extends ActionType ? Promise<void> : void
t: (key: string, replacements?: Record<string, string>) => string,
dispatchAction: <T extends ActionType | SyncActionType>(action: T, data: Parameters<(typeof Actions & typeof SyncActions)[T]>[0] extends {data: any} ? Parameters<(typeof Actions & typeof SyncActions)[T]>[0]['data'] : any) => T extends ActionType ? Promise<void> : void
}
// Copy time from AppContextType

View file

@ -2,7 +2,7 @@ import {formatNumber} from '../../utils/helpers';
import {useAppContext} from '../../AppContext';
const Pagination = () => {
const {pagination, dispatchAction} = useAppContext();
const {pagination, dispatchAction, t} = useAppContext();
const loadMore = () => {
dispatchAction('loadMoreComments', {});
@ -18,9 +18,11 @@ const Pagination = () => {
return null;
}
const text = left === 1 ? t('Show 1 previous comment') : t('Show {{amount}} previous comments', {amount: formatNumber(left)})
return (
<button className="text-md group mb-10 flex w-full items-center px-0 pb-2 pt-0 text-left font-sans font-semibold text-neutral-700 dark:text-white" data-testid="pagination-component" type="button" onClick={loadMore}>
<span className="flex h-[39px] w-full items-center justify-center whitespace-nowrap rounded-[6px] bg-[rgba(0,0,0,0.05)] px-3 py-2 text-center font-sans text-sm font-semibold text-neutral-700 outline-0 transition-[opacity,background] duration-150 hover:bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.08)] dark:text-neutral-100 dark:hover:bg-[rgba(255,255,255,0.1)]"> Show {formatNumber(left)} previous {left === 1 ? 'comment' : 'comments'}</span>
<span className="flex h-[39px] w-full items-center justify-center whitespace-nowrap rounded-[6px] bg-[rgba(0,0,0,0.05)] px-3 py-2 text-center font-sans text-sm font-semibold text-neutral-700 outline-0 transition-[opacity,background] duration-150 hover:bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.08)] dark:text-neutral-100 dark:hover:bg-[rgba(255,255,255,0.1)]"> {text}</span>
</button>
);
};

View file

@ -7,11 +7,11 @@ type Props = {
toggleReply: () => void;
};
const ReplyButton: React.FC<Props> = ({disabled, isReplying, toggleReply}) => {
const {member} = useAppContext();
const {member, t} = useAppContext();
return member ?
(<button className={`duration-50 group flex items-center font-sans text-sm outline-0 transition-all ease-linear ${isReplying ? 'text-[rgba(0,0,0,0.9)] dark:text-[rgba(255,255,255,0.9)]' : 'text-[rgba(0,0,0,0.5)] hover:text-[rgba(0,0,0,0.75)] dark:text-[rgba(255,255,255,0.5)] dark:hover:text-[rgba(255,255,255,0.25)]'}`} data-testid="reply-button" disabled={!!disabled} type="button" onClick={toggleReply}>
<ReplyIcon className={`mr-[6px] ${isReplying ? 'fill-[rgba(0,0,0,0.9)] stroke-[rgba(0,0,0,0.9)] dark:fill-[rgba(255,255,255,0.9)] dark:stroke-[rgba(255,255,255,0.9)]' : 'stroke-[rgba(0,0,0,0.5)] group-hover:stroke-[rgba(0,0,0,0.75)] dark:stroke-[rgba(255,255,255,0.5)] dark:group-hover:stroke-[rgba(255,255,255,0.25)]'} duration-50 transition ease-linear`} />Reply
<ReplyIcon className={`mr-[6px] ${isReplying ? 'fill-[rgba(0,0,0,0.9)] stroke-[rgba(0,0,0,0.9)] dark:fill-[rgba(255,255,255,0.9)] dark:stroke-[rgba(255,255,255,0.9)]' : 'stroke-[rgba(0,0,0,0.5)] group-hover:stroke-[rgba(0,0,0,0.75)] dark:stroke-[rgba(255,255,255,0.5)] dark:group-hover:stroke-[rgba(255,255,255,0.25)]'} duration-50 transition ease-linear`} />{t('Reply')}
</button>) : null;
};

View file

@ -19,8 +19,9 @@ export function useOptions(scriptTag: HTMLElement) {
const title = dataset.title === 'null' ? null : (dataset.title ?? ''); // Null means use the default title. Missing = no title.
const showCount = dataset.count === 'true';
const publication = dataset.publication ?? ''; // TODO: replace with dynamic data from script
const locale = dataset.locale ?? 'en';
const options = {siteUrl, apiKey, apiUrl, postId, adminUrl, colorScheme, avatarSaturation, accentColor, commentsEnabled, title, showCount, publication};
const options = {locale, siteUrl, apiKey, apiUrl, postId, adminUrl, colorScheme, avatarSaturation, accentColor, commentsEnabled, title, showCount, publication};
return options;
}, [scriptTag]);

View file

@ -67,6 +67,7 @@
"vite": "4.4.9",
"vite-plugin-commonjs": "0.8.2",
"vite-plugin-svgr": "3.2.0",
"vitest": "0.34.2"
"vitest": "0.34.2",
"@tryghost/i18n": "0.0.0"
}
}

View file

@ -1,5 +1,5 @@
const {SafeString} = require('../services/handlebars');
const {urlUtils, getFrontendKey, settingsCache} = require('../services/proxy');
const {labs, urlUtils, getFrontendKey, settingsCache} = require('../services/proxy');
const {getFrontendAppConfig, getDataAttributes} = require('../utils/frontend-apps');
module.exports = async function comments(options) {
@ -53,6 +53,7 @@ module.exports = async function comments(options) {
const {scriptUrl, stylesUrl} = getFrontendAppConfig('comments');
const data = {
locale: labs.isSet('i18n') ? (settingsCache.get('locale') || 'en') : undefined,
'ghost-comments': urlUtils.getSiteUrl(),
api: urlUtils.urlFor('api', {type: 'content'}, true),
admin: urlUtils.urlFor('admin', true),

View file

@ -24,6 +24,9 @@ function getDataAttributes(data) {
return dataAttributes;
}
Object.entries(data).forEach(([key, value]) => {
if (value === undefined) {
return;
}
dataAttributes += `data-${key}="${value}" `;
});

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -98,6 +98,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",
"Renews at {{price}}.": "Label for a discounted subscription, explains the price it will renew at",
"Reply": "Button to reply to a comment",
"Retry": "When something goes wrong, this link allows people to re-attempt the same action",
"Save": "A button to save",
"Secure sign in link for {{siteTitle}}": "The subject line of member login emails",
@ -107,6 +108,8 @@
"Sending login link...": "A loading status message when a member has just clicked to login",
"Sending...": "A loading status message when an email is being sent",
"Sent to {{email}}": "A confirmation message that an email has been sent",
"Show 1 previous comment": "Button in comments app to load previous comments, when there is only one",
"Show {{amount}} previous comments": "Button in comments appto load previous comments",
"Sign in": "A button to sign in",
"Sign in to {{siteTitle}}": "The title displayed on the login screen",
"Sign out": "A button to sign out",

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "Antwoorden",
"Show {{amount}} previous comments": "Toon {{amount}} vorige reacties",
"Show 1 previous comment": "Toon 1 vorige reactie"
}

View file

@ -99,7 +99,7 @@
"Sending login link...": "Inloglink versturen...",
"Sending...": "Versturen...",
"Sign in": "Inloggen",
"Sign out": "",
"Sign out": "Uitloggen",
"Sign up": "Registreren",
"Signup error: Invalid link": "",
"Sorry, that didnt work.": "",

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -0,0 +1,5 @@
{
"Reply": "",
"Show {{amount}} previous comments": "",
"Show 1 previous comment": ""
}

View file

@ -1855,13 +1855,20 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438"
integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.22.5":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682"
integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.20.7", "@babel/template@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
@ -26053,6 +26060,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
regenerator-runtime@^0.9.5:
version "0.9.6"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029"