Skip to content

Commit

Permalink
Fix /share and cleanup and reorganize frontend locale loading (#25240)
Browse files Browse the repository at this point in the history
  • Loading branch information
renchap committed Jun 2, 2023
1 parent 5fae2de commit b0780cf
Show file tree
Hide file tree
Showing 25 changed files with 152 additions and 638 deletions.
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ docker-compose.override.yml
/app/javascript/mastodon/features/emoji/emoji_map.json

# Ignore locale files
/app/javascript/mastodon/locales
/app/javascript/mastodon/locales/*.json
/config/locales

# Ignore vendored CSS reset
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/react_component_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def react_component(name, props = {}, &block)
end

def react_admin_component(name, props = {})
data = { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) }
data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) }
div_tag_with_data(data)
end

Expand Down
9 changes: 5 additions & 4 deletions app/javascript/mastodon/actions/streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import {
fillListTimelineGaps,
} from './timelines';

const { messages } = getLocale();

/**
* @param {number} max
* @returns {number}
Expand All @@ -43,8 +41,10 @@ const randomUpTo = max =>
* @param {function(object): boolean} [options.accept]
* @returns {function(): void}
*/
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) =>
connectStream(channelName, params, (dispatch, getState) => {
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
const { messages } = getLocale();

return connectStream(channelName, params, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);

// @ts-expect-error
Expand Down Expand Up @@ -121,6 +121,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
},
};
});
};

/**
* @param {Function} dispatch
Expand Down
11 changes: 3 additions & 8 deletions app/javascript/mastodon/containers/admin_component.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import { IntlProvider } from 'react-intl';

import { getLocale, onProviderError } from '../locales';

const { messages } = getLocale();
import { IntlProvider } from 'mastodon/locales';

export default class AdminComponent extends PureComponent {

static propTypes = {
locale: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};

render () {
const { locale, children } = this.props;
const { children } = this.props;

return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
<IntlProvider>
{children}
</IntlProvider>
);
Expand Down
16 changes: 3 additions & 13 deletions app/javascript/mastodon/containers/compose_container.jsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import { IntlProvider } from 'react-intl';

import { Provider } from 'react-redux';

import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store';
import Compose from '../features/standalone/compose';
import initialState from '../initial_state';
import { getLocale, onProviderError } from '../locales';
import { IntlProvider } from '../locales';
import { store } from '../store';

const { messages } = getLocale();

if (initialState) {
store.dispatch(hydrateStore(initialState));
}

store.dispatch(fetchCustomEmojis());

export default class TimelineContainer extends PureComponent {

static propTypes = {
locale: PropTypes.string.isRequired,
};
export default class ComposeContainer extends PureComponent {

render () {
const { locale } = this.props;

return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
<IntlProvider>
<Provider store={store}>
<Compose />
</Provider>
Expand Down
14 changes: 2 additions & 12 deletions app/javascript/mastodon/containers/mastodon.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import { IntlProvider } from 'react-intl';

import { Helmet } from 'react-helmet';
import { BrowserRouter, Route } from 'react-router-dom';

Expand All @@ -16,11 +14,9 @@ import { connectUserStream } from 'mastodon/actions/streaming';
import ErrorBoundary from 'mastodon/components/error_boundary';
import UI from 'mastodon/features/ui';
import initialState, { title as siteTitle } from 'mastodon/initial_state';
import { getLocale, onProviderError } from 'mastodon/locales';
import { IntlProvider } from 'mastodon/locales';
import { store } from 'mastodon/store';

const { messages } = getLocale();

const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;

const hydrateAction = hydrateStore(initialState);
Expand All @@ -40,10 +36,6 @@ const createIdentityContext = state => ({

export default class Mastodon extends PureComponent {

static propTypes = {
locale: PropTypes.string.isRequired,
};

static childContextTypes = {
identity: PropTypes.shape({
signedIn: PropTypes.bool.isRequired,
Expand Down Expand Up @@ -79,10 +71,8 @@ export default class Mastodon extends PureComponent {
}

render () {
const { locale } = this.props;

return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
<IntlProvider>
<ReduxProvider store={store}>
<ErrorBoundary>
<BrowserRouter>
Expand Down
11 changes: 3 additions & 8 deletions app/javascript/mastodon/containers/media_container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { createPortal } from 'react-dom';

import { IntlProvider } from 'react-intl';

import { fromJS } from 'immutable';

import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
Expand All @@ -14,17 +12,14 @@ import Audio from 'mastodon/features/audio';
import Card from 'mastodon/features/status/components/card';
import MediaModal from 'mastodon/features/ui/components/media_modal';
import Video from 'mastodon/features/video';
import { getLocale, onProviderError } from 'mastodon/locales';
import { IntlProvider } from 'mastodon/locales';
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';

const { messages } = getLocale();

const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };

export default class MediaContainer extends PureComponent {

static propTypes = {
locale: PropTypes.string.isRequired,
components: PropTypes.object.isRequired,
};

Expand Down Expand Up @@ -73,7 +68,7 @@ export default class MediaContainer extends PureComponent {
};

render () {
const { locale, components } = this.props;
const { components } = this.props;

let handleOpenVideo;

Expand All @@ -83,7 +78,7 @@ export default class MediaContainer extends PureComponent {
}

return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
<IntlProvider>
<>
{[].map.call(components, (component, i) => {
const componentName = component.getAttribute('data-component');
Expand Down
14 changes: 0 additions & 14 deletions app/javascript/mastodon/load_locale.js

This file was deleted.

22 changes: 22 additions & 0 deletions app/javascript/mastodon/locales/global_locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface LocaleData {
locale: string;
messages: Record<string, string>;
}

let loadedLocale: LocaleData;

export function setLocale(locale: LocaleData) {
loadedLocale = locale;
}

export function getLocale() {
if (!loadedLocale && process.env.NODE_ENV === 'development') {
throw new Error('getLocale() called before any locale has been set');
}

return loadedLocale;
}

export function isLocaleLoaded() {
return !!loadedLocale;
}
22 changes: 0 additions & 22 deletions app/javascript/mastodon/locales/index.js

This file was deleted.

5 changes: 5 additions & 0 deletions app/javascript/mastodon/locales/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type { LocaleData } from './global_locale';
export { setLocale, getLocale, isLocaleLoaded } from './global_locale';
export { loadLocale } from './load_locale';

export { IntlProvider } from './intl_provider';
56 changes: 56 additions & 0 deletions app/javascript/mastodon/locales/intl_provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useState } from 'react';

import { IntlProvider as BaseIntlProvider } from 'react-intl';

import { getLocale, isLocaleLoaded } from './global_locale';
import { loadLocale } from './load_locale';

function onProviderError(error: unknown) {
// Silent the error, like upstream does
if (process.env.NODE_ENV === 'production') return;

// This browser does not advertise Intl support for this locale, we only print a warning
// As-per the spec, the browser should select the best matching locale
if (
error &&
typeof error === 'object' &&
error instanceof Error &&
error.message.match('MISSING_DATA')
) {
console.warn(error.message);
}

console.error(error);
}

export const IntlProvider: React.FC<
Omit<React.ComponentProps<typeof BaseIntlProvider>, 'locale' | 'messages'>
> = ({ children, ...props }) => {
const [localeLoaded, setLocaleLoaded] = useState(false);

useEffect(() => {
async function loadLocaleData() {
if (!isLocaleLoaded()) {
await loadLocale();
}

setLocaleLoaded(true);
}
void loadLocaleData();
}, []);

if (!localeLoaded) return null;

const { locale, messages } = getLocale();

return (
<BaseIntlProvider
locale={locale}
messages={messages}
onError={onProviderError}
{...props}
>
{children}
</BaseIntlProvider>
);
};
29 changes: 29 additions & 0 deletions app/javascript/mastodon/locales/load_locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Semaphore } from 'async-mutex';

import type { LocaleData } from './global_locale';
import { isLocaleLoaded, setLocale } from './global_locale';

const localeLoadingSemaphore = new Semaphore(1);

export async function loadLocale() {
const locale = document.querySelector<HTMLElement>('html')?.lang || 'en';

// We use a Semaphore here so only one thing can try to load the locales at
// the same time. If one tries to do it while its in progress, it will wait
// for the initial load to finish before it is resumed (and will see that locale
// data is already loaded)
await localeLoadingSemaphore.runExclusive(async () => {
// if the locale is already set, then do nothing
if (isLocaleLoaded()) return;

const localeData = (await import(
/* webpackMode: "lazy" */
/* webpackChunkName: "locale/[request]" */
/* webpackInclude: /\.json$/ */
/* webpackPreload: true */
`mastodon/locales/${locale}.json`
)) as LocaleData['messages'];

setLocale({ messages: localeData, locale });
});
}

0 comments on commit b0780cf

Please sign in to comment.