diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index e16a7409e..f1c44d2e2 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -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 diff --git a/app/javascript/flavours/glitch/containers/admin_component.jsx b/app/javascript/flavours/glitch/containers/admin_component.jsx index a4dbe9c43..06c846f4d 100644 --- a/app/javascript/flavours/glitch/containers/admin_component.jsx +++ b/app/javascript/flavours/glitch/containers/admin_component.jsx @@ -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 ( - + {children} ); diff --git a/app/javascript/flavours/glitch/containers/compose_container.jsx b/app/javascript/flavours/glitch/containers/compose_container.jsx index f053fc6d4..f92bf9797 100644 --- a/app/javascript/flavours/glitch/containers/compose_container.jsx +++ b/app/javascript/flavours/glitch/containers/compose_container.jsx @@ -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 ( - + diff --git a/app/javascript/flavours/glitch/containers/mastodon.jsx b/app/javascript/flavours/glitch/containers/mastodon.jsx index fc09582e6..ae2eb0b60 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.jsx +++ b/app/javascript/flavours/glitch/containers/mastodon.jsx @@ -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 ( - + diff --git a/app/javascript/flavours/glitch/containers/media_container.jsx b/app/javascript/flavours/glitch/containers/media_container.jsx index 99447aebd..52aac5ebe 100644 --- a/app/javascript/flavours/glitch/containers/media_container.jsx +++ b/app/javascript/flavours/glitch/containers/media_container.jsx @@ -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 ( - + <> {[].map.call(components, (component, i) => { const componentName = component.getAttribute('data-component'); diff --git a/app/javascript/flavours/glitch/load_locale.js b/app/javascript/flavours/glitch/load_locale.js deleted file mode 100644 index 1c22bfd31..000000000 --- a/app/javascript/flavours/glitch/load_locale.js +++ /dev/null @@ -1,21 +0,0 @@ -import { setLocale } from 'locales'; - -export async function loadLocale() { - const locale = document.querySelector('html').lang || 'en'; - - const upstreamLocaleData = await import( - /* webpackMode: "lazy" */ - /* webpackChunkName: "locales/vanilla/[request]" */ - /* webpackInclude: /\.json$/ */ - /* webpackPreload: true */ - `mastodon/locales/${locale}.json`); - - const localeData = await import( - /* webpackMode: "lazy" */ - /* webpackChunkName: "locales/glitch/[request]" */ - /* webpackInclude: /\.json$/ */ - /* webpackPreload: true */ - `flavours/glitch/locales/${locale}.json`); - - setLocale({ messages: {...upstreamLocaleData, ...localeData} }); -} diff --git a/app/javascript/flavours/glitch/locales/global_locale.ts b/app/javascript/flavours/glitch/locales/global_locale.ts new file mode 100644 index 000000000..01133ca23 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/global_locale.ts @@ -0,0 +1,22 @@ +export interface LocaleData { + locale: string; + messages: Record; +} + +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; +} diff --git a/app/javascript/flavours/glitch/locales/index.ts b/app/javascript/flavours/glitch/locales/index.ts new file mode 100644 index 000000000..63f45c304 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/index.ts @@ -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'; diff --git a/app/javascript/flavours/glitch/locales/intl_provider.tsx b/app/javascript/flavours/glitch/locales/intl_provider.tsx new file mode 100644 index 000000000..1ea77c798 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/intl_provider.tsx @@ -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, '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 ( + + {children} + + ); +}; diff --git a/app/javascript/flavours/glitch/locales/load_locale.ts b/app/javascript/flavours/glitch/locales/load_locale.ts new file mode 100644 index 000000000..fa5d13bbc --- /dev/null +++ b/app/javascript/flavours/glitch/locales/load_locale.ts @@ -0,0 +1,37 @@ +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'; + + // 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` + ) as LocaleData['messages']; + + 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 }, locale }); + }); +} diff --git a/app/javascript/flavours/glitch/packs/admin.jsx b/app/javascript/flavours/glitch/packs/admin.jsx index 4a5a50b49..596611dad 100644 --- a/app/javascript/flavours/glitch/packs/admin.jsx +++ b/app/javascript/flavours/glitch/packs/admin.jsx @@ -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 ( - + , ); diff --git a/app/javascript/flavours/glitch/packs/home.js b/app/javascript/flavours/glitch/packs/home.js index 5497ba85e..842430354 100644 --- a/app/javascript/flavours/glitch/packs/home.js +++ b/app/javascript/flavours/glitch/packs/home.js @@ -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); + }); diff --git a/app/javascript/flavours/glitch/packs/public.jsx b/app/javascript/flavours/glitch/packs/public.jsx index 4edc06b38..22a3e1214 100644 --- a/app/javascript/flavours/glitch/packs/public.jsx +++ b/app/javascript/flavours/glitch/packs/public.jsx @@ -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(); diff --git a/app/javascript/flavours/glitch/packs/share.jsx b/app/javascript/flavours/glitch/packs/share.jsx index 1b8c837b6..21f1da638 100644 --- a/app/javascript/flavours/glitch/packs/share.jsx +++ b/app/javascript/flavours/glitch/packs/share.jsx @@ -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); });