M app/javascript/flavours/glitch/actions/streaming.js => app/javascript/flavours/glitch/actions/streaming.js +6 -5
@@ 1,6 1,6 @@
// @ts-check
-import { getLocale } from 'mastodon/locales';
+import { getLocale } from 'flavours/glitch/locales';
import { connectStream } from '../stream';
@@ 25,8 25,6 @@ import {
fillListTimelineGaps,
} from './timelines';
-const { messages } = getLocale();
-
/**
* @param {number} max
* @returns {number}
@@ 44,8 42,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
@@ 122,6 122,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
},
};
});
+};
/**
* @param {Function} dispatch
M app/javascript/flavours/glitch/containers/admin_component.jsx => app/javascript/flavours/glitch/containers/admin_component.jsx +3 -8
@@ 1,24 1,19 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
-import { IntlProvider } from 'react-intl';
-
-import { getLocale, onProviderError } from 'mastodon/locales';
-
-const { messages } = getLocale();
+import { IntlProvider } from 'flavours/glitch/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>
);
M app/javascript/flavours/glitch/containers/compose_container.jsx => app/javascript/flavours/glitch/containers/compose_container.jsx +3 -15
@@ 1,37 1,25 @@
-import PropTypes from 'prop-types';
import { PureComponent } from 'react';
-import { IntlProvider } from 'react-intl';
-
import { Provider } from 'react-redux';
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
import { hydrateStore } from 'flavours/glitch/actions/store';
import Compose from 'flavours/glitch/features/standalone/compose';
import initialState from 'flavours/glitch/initial_state';
+import { IntlProvider } from 'flavours/glitch/locales';
import { store } from 'flavours/glitch/store';
-import { getLocale, onProviderError } from 'mastodon/locales';
-
-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>
M app/javascript/flavours/glitch/containers/mastodon.jsx => app/javascript/flavours/glitch/containers/mastodon.jsx +2 -12
@@ 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';
@@ 17,10 15,8 @@ import { connectUserStream } from 'flavours/glitch/actions/streaming';
import ErrorBoundary from 'flavours/glitch/components/error_boundary';
import UI from 'flavours/glitch/features/ui';
import initialState, { title as siteTitle } from 'flavours/glitch/initial_state';
+import { IntlProvider } from 'flavours/glitch/locales';
import { store } from 'flavours/glitch/store';
-import { getLocale, onProviderError } from 'locales';
-
-const { messages } = getLocale();
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
@@ 44,10 40,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,
@@ 83,10 75,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>
M app/javascript/flavours/glitch/containers/media_container.jsx => app/javascript/flavours/glitch/containers/media_container.jsx +3 -9
@@ 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 'flavours/glitch/components/hashtag';
@@ 14,18 12,14 @@ import Audio from 'flavours/glitch/features/audio';
import Card from 'flavours/glitch/features/status/components/card';
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
import Video from 'flavours/glitch/features/video';
+import { IntlProvider } from 'flavours/glitch/locales';
import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar';
-import { getLocale, onProviderError } from 'mastodon/locales';
-
-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,
};
@@ 74,7 68,7 @@ export default class MediaContainer extends PureComponent {
};
render () {
- const { locale, components } = this.props;
+ const { components } = this.props;
let handleOpenVideo;
@@ 84,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');
A app/javascript/flavours/glitch/locales/global_locale.ts => app/javascript/flavours/glitch/locales/global_locale.ts +22 -0
@@ 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;
+}
A app/javascript/flavours/glitch/locales/index.ts => app/javascript/flavours/glitch/locales/index.ts +5 -0
@@ 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';
A app/javascript/flavours/glitch/locales/intl_provider.tsx => app/javascript/flavours/glitch/locales/intl_provider.tsx +56 -0
@@ 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>
+ );
+};
R app/javascript/flavours/glitch/load_locale.js => app/javascript/flavours/glitch/locales/load_locale.ts +31 -15
@@ 1,21 1,37 @@
-import { setLocale } from 'locales';
+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('html').lang || 'en';
+ 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 upstreamLocaleData = await import(
- /* webpackMode: "lazy" */
- /* webpackChunkName: "locales/vanilla/[request]" */
- /* webpackInclude: /\.json$/ */
- /* webpackPreload: true */
- `mastodon/locales/${locale}.json`);
+ const upstreamLocaleData = await import(
+ /* webpackMode: "lazy" */
+ /* webpackChunkName: "locales/vanilla/[request]" */
+ /* webpackInclude: /\.json$/ */
+ /* webpackPreload: true */
+ `mastodon/locales/${locale}.json`
+ ) as LocaleData['messages'];
- const localeData = await import(
- /* webpackMode: "lazy" */
- /* webpackChunkName: "locales/glitch/[request]" */
- /* webpackInclude: /\.json$/ */
- /* webpackPreload: true */
- `flavours/glitch/locales/${locale}.json`);
+ const localeData = await import(
+ /* webpackMode: "lazy" */
+ /* webpackChunkName: "locales/glitch/[request]" */
+ /* webpackInclude: /\.json$/ */
+ /* webpackPreload: true */
+ `flavours/glitch/locales/${locale}.json`
+ ) as LocaleData['messages'];
- setLocale({ messages: {...upstreamLocaleData, ...localeData} });
+ setLocale({ messages: { ...upstreamLocaleData, ...localeData }, locale });
+ });
}
M app/javascript/flavours/glitch/packs/admin.jsx => app/javascript/flavours/glitch/packs/admin.jsx +2 -2
@@ 6,14 6,14 @@ import ready from 'flavours/glitch/ready';
ready(() => {
[].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
const componentName = element.getAttribute('data-admin-component');
- const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props'));
+ const { ...componentProps } = JSON.parse(element.getAttribute('data-props'));
import('flavours/glitch/containers/admin_component').then(({ default: AdminComponent }) => {
return import('flavours/glitch/components/admin/' + componentName).then(({ default: Component }) => {
const root = createRoot(element);
root.render (
- <AdminComponent locale={locale}>
+ <AdminComponent>
<Component {...componentProps} />
</AdminComponent>,
);
M app/javascript/flavours/glitch/packs/home.js => app/javascript/flavours/glitch/packs/home.js +8 -8
@@ 1,11 1,11 @@
import 'packs/public-path';
-import { loadLocale } from 'flavours/glitch/load_locale';
+import { loadLocale } from 'flavours/glitch/locales';
+import main from "flavours/glitch/main";
import { loadPolyfills } from 'flavours/glitch/polyfills';
-loadPolyfills().then(loadLocale).then(async () => {
- const { default: main } = await import('flavours/glitch/main');
-
- return main();
-}).catch(e => {
- console.error(e);
-});
+loadPolyfills()
+ .then(loadLocale)
+ .then(main)
+ .catch(e => {
+ console.error(e);
+ });
M app/javascript/flavours/glitch/packs/public.jsx => app/javascript/flavours/glitch/packs/public.jsx +3 -4
@@ 1,7 1,7 @@
import 'packs/public-path';
import { createRoot } from 'react-dom/client';
-import * as IntlMessageFormat from 'intl-messageformat';
+import { IntlMessageFormat } from 'intl-messageformat';
import { defineMessages } from 'react-intl';
import { delegate } from '@rails/ujs';
@@ 12,10 12,9 @@ import { throttle } from 'lodash';
import { timeAgoString } from 'flavours/glitch/components/relative_timestamp';
import emojify from 'flavours/glitch/features/emoji/emoji';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
-import { loadLocale } from 'flavours/glitch/load_locale';
+import { loadLocale, getLocale } from 'flavours/glitch/locales';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready';
-import { getLocale } from 'locales';
const messages = defineMessages({
usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
@@ 24,7 23,7 @@ const messages = defineMessages({
});
function main() {
- const { localeData } = getLocale();
+ const { messages: localeData } = getLocale();
const scrollToDetailedStatus = () => {
const history = createBrowserHistory();
M app/javascript/flavours/glitch/packs/share.jsx => app/javascript/flavours/glitch/packs/share.jsx +1 -2
@@ 2,7 2,6 @@ import 'packs/public-path';
import { createRoot } from 'react-dom/client';
import ComposeContainer from 'flavours/glitch/containers/compose_container';
-import { loadLocale } from 'flavours/glitch/load_locale';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready';
@@ 23,6 22,6 @@ function main() {
ready(loaded);
}
-loadPolyfills().then(loadLocale).then(main).catch(error => {
+loadPolyfills().then(main).catch(error => {
console.error(error);
});