Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Microcopy from Assets API #1510

Merged
merged 11 commits into from
May 16, 2022
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ way to update this template, but currently, we follow a pattern:

## Upcoming version 2022-XX-XX

- [add] Add support for hosted translations.

- This PR fetches "content/translation.json" from a new Asset Delivery API. The file is editable
through the Flex Console.
- It also adds all the missing translation keys to existing non-English translation files. This
means that those files might now include messages in English.

[#1510](https://github.com/sharetribe/ftw-daily/pull/1510)

- [delete] Remove old unused translation keys.
[#1511](https://github.com/sharetribe/ftw-daily/pull/1511)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"redux": "^4.1.2",
"redux-thunk": "^2.4.1",
"seedrandom": "^3.0.5",
"sharetribe-flex-sdk": "^1.15.0",
"sharetribe-flex-sdk": "^1.17.0",
"sharetribe-scripts": "5.0.1",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.21",
Expand Down
6 changes: 6 additions & 0 deletions server/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ const data = 'data:';
const blob = 'blob:';
const devImagesMaybe = dev ? ['*.localhost:8000'] : [];
const baseUrl = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL || 'https://flex-api.sharetribe.com';
// Asset Delivery API is using a different domain than other Flex APIs
// cdn.st-api.com
// If assetCdnBaseUrl is used to initialize SDK (for proxy purposes), then that URL needs to be in CSP
const assetCdnBaseUrl = process.env.REACT_APP_SHARETRIBE_SDK_ASSET_CDN_BASE_URL;

// Default CSP whitelist.
//
Expand All @@ -20,6 +24,8 @@ const defaultDirectives = {
connectSrc: [
self,
baseUrl,
assetCdnBaseUrl,
'*.st-api.com',
'maps.googleapis.com',
'*.tiles.mapbox.com',
'api.mapbox.com',
Expand Down
17 changes: 14 additions & 3 deletions server/dataLoader.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const url = require('url');
const log = require('./log');

exports.loadData = function(requestUrl, sdk, matchPathname, configureStore, routeConfiguration) {
exports.loadData = function(requestUrl, sdk, appInfo) {
const { matchPathname, configureStore, routeConfiguration, config, fetchAppAssets } = appInfo;
const { pathname, query } = url.parse(requestUrl);
const matchedRoutes = matchPathname(pathname, routeConfiguration());

let translations = {};
const store = configureStore({}, sdk);

const dataLoadingCalls = matchedRoutes.reduce((calls, match) => {
Expand All @@ -15,9 +17,18 @@ exports.loadData = function(requestUrl, sdk, matchPathname, configureStore, rout
return calls;
}, []);

return Promise.all(dataLoadingCalls)
// First fetch app-wide assets
// Then make loadData calls
// And return object containing preloaded state and translations
// This order supports other asset (in the future) that should be fetched before data calls.
return store
.dispatch(fetchAppAssets(config.appCdnAssets))
.then(fetchedAssets => {
translations = fetchedAssets?.translations?.data || {};
return Promise.all(dataLoadingCalls);
})
.then(() => {
return store.getState();
return { preloadedState: store.getState(), translations };
})
.catch(e => {
log.error(e, 'server-side-data-load-failed');
Expand Down
11 changes: 7 additions & 4 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const dev = process.env.REACT_APP_ENV === 'development';
const PORT = parseInt(process.env.PORT, 10);
const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL;
const ASSET_CDN_BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_ASSET_CDN_BASE_URL;
const TRANSIT_VERBOSE = process.env.REACT_APP_SHARETRIBE_SDK_TRANSIT_VERBOSE === 'true';
const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';
const TRUST_PROXY = process.env.SERVER_SHARETRIBE_TRUST_PROXY || null;
Expand Down Expand Up @@ -199,6 +200,7 @@ app.get('*', (req, res) => {
});

const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};
const assetCdnBaseUrl = ASSET_CDN_BASE_URL ? { assetCdnBaseUrl: ASSET_CDN_BASE_URL } : {};

const sdk = sharetribeSdk.createInstance({
transitVerbose: TRANSIT_VERBOSE,
Expand All @@ -208,6 +210,7 @@ app.get('*', (req, res) => {
tokenStore,
typeHandlers: sdkUtils.typeHandlers,
...baseUrl,
...assetCdnBaseUrl,
});

// Until we have a better plan for caching dynamic content and we
Expand All @@ -221,12 +224,12 @@ app.get('*', (req, res) => {

// Server-side entrypoint provides us the functions for server-side data loading and rendering
const nodeEntrypoint = nodeExtractor.requireEntrypoint();
const { default: renderApp, matchPathname, configureStore, routeConfiguration } = nodeEntrypoint;
const { default: renderApp, ...appInfo } = nodeEntrypoint;

dataLoader
.loadData(req.url, sdk, matchPathname, configureStore, routeConfiguration)
.then(preloadedState => {
const html = renderer.render(req.url, context, preloadedState, renderApp, webExtractor);
.loadData(req.url, sdk, appInfo)
.then(data => {
const html = renderer.render(req.url, context, data, renderApp, webExtractor);

if (dev) {
const debugData = {
Expand Down
13 changes: 11 additions & 2 deletions server/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,20 @@ const replacer = (key = null, value) => {
return types.replacer(key, cleanedValue);
};

exports.render = function(requestUrl, context, preloadedState, renderApp, webExtractor) {
exports.render = function(requestUrl, context, data, renderApp, webExtractor) {
const { preloadedState, translations } = data;

// Bind webExtractor as "this" for collectChunks call.
const collectWebChunks = webExtractor.collectChunks.bind(webExtractor);

const { head, body } = renderApp(requestUrl, context, preloadedState, collectWebChunks);
// Render the app with given route, preloaded state, hosted translations.
const { head, body } = renderApp(
requestUrl,
context,
preloadedState,
translations,
collectWebChunks
);

// Preloaded state needs to be passed for client side too.
// For security reasons we ensure that preloaded state is considered as a string
Expand Down
62 changes: 43 additions & 19 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import routeConfiguration from './routeConfiguration';
import Routes from './Routes';
import config from './config';

// Flex template application uses English translations as default.
// Flex template application uses English translations as default translations.
import defaultMessages from './translations/en.json';

// If you want to change the language, change the imports to match the wanted locale:
// If you want to change the language of default (fallback) translations,
// change the imports to match the wanted locale:
//
// 1) Change the language in the config.js file!
// 2) Import correct locale rules for Moment library
// 3) Use the `messagesInLocale` import to add the correct translation file.
Expand All @@ -48,6 +50,11 @@ const messagesInLocale = {};
const addMissingTranslations = (sourceLangTranslations, targetLangTranslations) => {
const sourceKeys = Object.keys(sourceLangTranslations);
const targetKeys = Object.keys(targetLangTranslations);

// if there's no translations defined for target language, return source translations
if (targetKeys.length === 0) {
return sourceLangTranslations;
}
const missingKeys = difference(sourceKeys, targetKeys);

const addMissingTranslation = (translations, missingKey) => ({
Expand All @@ -58,18 +65,15 @@ const addMissingTranslations = (sourceLangTranslations, targetLangTranslations)
return missingKeys.reduce(addMissingTranslation, targetLangTranslations);
};

const isDefaultLanguageInUse = config.locale === 'en';

const messages = isDefaultLanguageInUse
? defaultMessages
: addMissingTranslations(defaultMessages, messagesInLocale);

// Get default messages for a given locale.
//
// Note: Locale should not affect the tests. We ensure this by providing
// messages with the key as the value of each message and discard the value.
// { 'My.translationKey1': 'My.translationKey1', 'My.translationKey2': 'My.translationKey2' }
const isTestEnv = process.env.NODE_ENV === 'test';

// Locale should not affect the tests. We ensure this by providing
// messages with the key as the value of each message.
const testMessages = mapValues(messages, (val, key) => key);
const localeMessages = isTestEnv ? testMessages : messages;
const localeMessages = isTestEnv
? mapValues(defaultMessages, (val, key) => key)
: addMissingTranslations(defaultMessages, messagesInLocale);

const setupLocale = () => {
if (isTestEnv) {
Expand All @@ -85,10 +89,14 @@ const setupLocale = () => {
};

export const ClientApp = props => {
const { store } = props;
const { store, hostedTranslations = {} } = props;
setupLocale();
return (
<IntlProvider locale={config.locale} messages={localeMessages} textComponent="span">
<IntlProvider
locale={config.locale}
messages={{ ...localeMessages, ...hostedTranslations }}
textComponent="span"
>
<Provider store={store}>
<HelmetProvider>
<BrowserRouter>
Expand All @@ -105,11 +113,15 @@ const { any, string } = PropTypes;
ClientApp.propTypes = { store: any.isRequired };

export const ServerApp = props => {
const { url, context, helmetContext, store } = props;
const { url, context, helmetContext, store, hostedTranslations = {} } = props;
setupLocale();
HelmetProvider.canUseDOM = false;
return (
<IntlProvider locale={config.locale} messages={localeMessages} textComponent="span">
<IntlProvider
locale={config.locale}
messages={{ ...localeMessages, ...hostedTranslations }}
textComponent="span"
>
<Provider store={store}>
<HelmetProvider context={helmetContext}>
<StaticRouter location={url} context={context}>
Expand All @@ -133,7 +145,13 @@ ServerApp.propTypes = { url: string.isRequired, context: any.isRequired, store:
* - {String} body: Rendered application body of the given route
* - {Object} head: Application head metadata from react-helmet
*/
export const renderApp = (url, serverContext, preloadedState, collectChunks) => {
export const renderApp = (
url,
serverContext,
preloadedState,
hostedTranslations,
collectChunks
) => {
// Don't pass an SDK instance since we're only rendering the
// component tree with the preloaded store state and components
// shouldn't do any SDK calls in the (server) rendering lifecycle.
Expand All @@ -145,7 +163,13 @@ export const renderApp = (url, serverContext, preloadedState, collectChunks) =>
// This is needed to figure out correct chunks/scripts to be included to server-rendered page.
// https://loadable-components.com/docs/server-side-rendering/#3-setup-chunkextractor-server-side
const WithChunks = collectChunks(
<ServerApp url={url} context={serverContext} helmetContext={helmetContext} store={store} />
<ServerApp
url={url}
context={serverContext}
helmetContext={helmetContext}
store={store}
hostedTranslations={hostedTranslations}
/>
);
const body = ReactDOMServer.renderToString(WithChunks);
const { helmet: head } = helmetContext;
Expand Down
13 changes: 9 additions & 4 deletions src/components/SectionHero/SectionHero.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { string } from 'prop-types';
import { FormattedMessage } from '../../util/reactIntl';
import classNames from 'classnames';
Expand All @@ -7,17 +7,22 @@ import { NamedLink } from '../../components';
import css from './SectionHero.module.css';

const SectionHero = props => {
const [mounted, setMounted] = useState(false);
const { rootClassName, className } = props;

useEffect(() => {
setMounted(true);
}, []);

const classes = classNames(rootClassName || css.root, className);

return (
<div className={classes}>
<div className={css.heroContent}>
<h1 className={css.heroMainTitle}>
<h1 className={classNames(css.heroMainTitle, { [css.heroMainTitleFEDelay]: mounted })}>
<FormattedMessage id="SectionHero.title" />
</h1>
<h2 className={css.heroSubTitle}>
<h2 className={classNames(css.heroSubTitle, { [css.heroSubTitleFEDelay]: mounted })}>
<FormattedMessage id="SectionHero.subTitle" />
</h2>
<NamedLink
Expand All @@ -26,7 +31,7 @@ const SectionHero = props => {
search:
'address=Finland&bounds=70.0922932%2C31.5870999%2C59.693623%2C20.456500199999937',
}}
className={css.heroButton}
className={classNames(css.heroButton, { [css.heroButtonFEDelay]: mounted })}
>
<FormattedMessage id="SectionHero.browseButton" />
</NamedLink>
Expand Down
16 changes: 10 additions & 6 deletions src/components/SectionHero/SectionHero.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
animation-duration: 0.5s;
animation-timing-function: ease-out;
-webkit-animation-fill-mode: forwards;
animation-delay: 3s;

visibility: hidden;
opacity: 1;
Expand Down Expand Up @@ -61,38 +62,41 @@
.heroMainTitle {
@apply --marketplaceHeroTitleFontStyles;
color: var(--matterColorLight);

composes: animation;
animation-delay: 0.5s;

@media (--viewportMedium) {
max-width: var(--SectionHero_desktopTitleMaxWidth);
}
}
.heroMainTitleFEDelay {
animation-delay: 0s;
}

.heroSubTitle {
@apply --marketplaceH4FontStyles;

color: var(--matterColorLight);
margin: 0 0 32px 0;

composes: animation;
animation-delay: 0.65s;

@media (--viewportMedium) {
max-width: var(--SectionHero_desktopTitleMaxWidth);
margin: 0 0 47px 0;
}
}
.heroSubTitleFEDelay {
animation-delay: 0.15s;
}

.heroButton {
@apply --marketplaceButtonStyles;
composes: animation;

animation-delay: 0.8s;

@media (--viewportMedium) {
display: block;
width: 260px;
}
}
.heroButtonFEDelay {
animation-delay: 0.3s;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ exports[`SectionHero matches snapshot 1`] = `
className="heroContent"
>
<h1
className="heroMainTitle"
className="heroMainTitle heroMainTitleFEDelay"
>
<span>
SectionHero.title
</span>
</h1>
<h2
className="heroSubTitle"
className="heroSubTitle heroSubTitleFEDelay"
>
<span>
SectionHero.subTitle
</span>
</h2>
<a
className="heroButton"
className="heroButton heroButtonFEDelay"
href="/s?address=Finland&bounds=70.0922932%2C31.5870999%2C59.693623%2C20.456500199999937"
onClick={[Function]}
onMouseOver={[Function]}
Expand Down
Loading