Merge commit 'b0780cfeeda641645ea65da257a72ec507e71647' into glitch-soc/merge-upstream
Conflicts: - `app/javascript/mastodon/load_locale.js`: The file moved to `app/javascript/mastodon/locales/load_locale.ts`. Ported the changes there and deleted `app/javascript/mastodon/load_locale.js`. - `app/javascript/mastodon/locales/index.js`: The file moved to `app/javascript/mastodon/locales/index.ts`. Did *not* port the changes as I want to try something a bit different.
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ module ReactComponentHelper | ||||
|   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 | ||||
|  | ||||
|   | ||||
| @@ -24,8 +24,6 @@ import { | ||||
|   fillListTimelineGaps, | ||||
| } from './timelines'; | ||||
|  | ||||
| const { messages } = getLocale(); | ||||
|  | ||||
| /** | ||||
|  * @param {number} max | ||||
|  * @returns {number} | ||||
| @@ -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 | ||||
| @@ -121,6 +121,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti | ||||
|       }, | ||||
|     }; | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @param {Function} dispatch | ||||
|   | ||||
| @@ -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> | ||||
|     ); | ||||
|   | ||||
| @@ -1,18 +1,14 @@ | ||||
| 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)); | ||||
| @@ -20,17 +16,11 @@ if (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> | ||||
|   | ||||
| @@ -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'; | ||||
|  | ||||
| @@ -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); | ||||
| @@ -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, | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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'; | ||||
| @@ -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, | ||||
|   }; | ||||
|  | ||||
| @@ -73,7 +68,7 @@ export default class MediaContainer extends PureComponent { | ||||
|   }; | ||||
|  | ||||
|   render () { | ||||
|     const { locale, components } = this.props; | ||||
|     const { components } = this.props; | ||||
|  | ||||
|     let handleOpenVideo; | ||||
|  | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -1,14 +0,0 @@ | ||||
| import { setLocale } from "./locales"; | ||||
|  | ||||
| export async function loadLocale() { | ||||
|   const locale = document.querySelector('html').lang || 'en'; | ||||
|  | ||||
|   const localeData = await import( | ||||
|     /* webpackMode: "lazy" */ | ||||
|     /* webpackChunkName: "locales/vanilla/[request]" */ | ||||
|     /* webpackInclude: /\.json$/ */ | ||||
|     /* webpackPreload: true */ | ||||
|     `mastodon/locales/${locale}.json`); | ||||
|  | ||||
|   setLocale({ messages: localeData }); | ||||
| } | ||||
							
								
								
									
										22
									
								
								app/javascript/mastodon/locales/global_locale.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/javascript/mastodon/locales/global_locale.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
| } | ||||
| @@ -1 +0,0 @@ | ||||
| export * from 'locales'; | ||||
							
								
								
									
										5
									
								
								app/javascript/mastodon/locales/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/javascript/mastodon/locales/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								app/javascript/mastodon/locales/intl_provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/javascript/mastodon/locales/intl_provider.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								app/javascript/mastodon/locales/load_locale.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/javascript/mastodon/locales/load_locale.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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: "locales/vanilla/[request]" */ | ||||
|       /* webpackInclude: /\.json$/ */ | ||||
|       /* webpackPreload: true */ | ||||
|       `mastodon/locales/${locale}.json` | ||||
|     )) as LocaleData['messages']; | ||||
|  | ||||
|     setLocale({ messages: localeData, locale }); | ||||
|   }); | ||||
| } | ||||
| @@ -1,221 +0,0 @@ | ||||
| # Custom Locale Data | ||||
|  | ||||
| This folder is used to store custom locale data. These custom locale data are | ||||
| not yet provided by [Unicode Common Locale Data Repository](http://cldr.unicode.org/development/new-cldr-developers) | ||||
| and hence not provided in [react-intl/locale-data/*](https://github.com/yahoo/react-intl). | ||||
|  | ||||
| The locale data should support [Locale Data APIs](https://github.com/yahoo/react-intl/wiki/API#locale-data-apis) | ||||
| of the react-intl library. | ||||
|  | ||||
| It is recommended to start your custom locale data from this sample English | ||||
| locale data ([*](#plural-rules)): | ||||
|  | ||||
| ```javascript | ||||
| /*eslint eqeqeq: "off"*/ | ||||
| /*eslint no-nested-ternary: "off"*/ | ||||
|  | ||||
| export default [ | ||||
|   { | ||||
|     locale: "en", | ||||
|     pluralRuleFunction: function(e, a) { | ||||
|       var n = String(e).split("."), | ||||
|         l = !n[1], | ||||
|         o = Number(n[0]) == e, | ||||
|         t = o && n[0].slice(-1), | ||||
|         r = o && n[0].slice(-2); | ||||
|       return a ? 1 == t && 11 != r ? "one" : 2 == t && 12 != r ? "two" : 3 == t && 13 != r ? "few" : "other" : 1 == e && l ? "one" : "other" | ||||
|     }, | ||||
|     fields: { | ||||
|       year: { | ||||
|         displayName: "year", | ||||
|         relative: { | ||||
|           0: "this year", | ||||
|           1: "next year", | ||||
|           "-1": "last year" | ||||
|         }, | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             one: "in {0} year", | ||||
|             other: "in {0} years" | ||||
|           }, | ||||
|           past: { | ||||
|             one: "{0} year ago", | ||||
|             other: "{0} years ago" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       month: { | ||||
|         displayName: "month", | ||||
|         relative: { | ||||
|           0: "this month", | ||||
|           1: "next month", | ||||
|           "-1": "last month" | ||||
|         }, | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             one: "in {0} month", | ||||
|             other: "in {0} months" | ||||
|           }, | ||||
|           past: { | ||||
|             one: "{0} month ago", | ||||
|             other: "{0} months ago" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       day: { | ||||
|         displayName: "day", | ||||
|         relative: { | ||||
|           0: "today", | ||||
|           1: "tomorrow", | ||||
|           "-1": "yesterday" | ||||
|         }, | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             one: "in {0} day", | ||||
|             other: "in {0} days" | ||||
|           }, | ||||
|           past: { | ||||
|             one: "{0} day ago", | ||||
|             other: "{0} days ago" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       hour: { | ||||
|         displayName: "hour", | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             one: "in {0} hour", | ||||
|             other: "in {0} hours" | ||||
|           }, | ||||
|           past: { | ||||
|             one: "{0} hour ago", | ||||
|             other: "{0} hours ago" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       minute: { | ||||
|         displayName: "minute", | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             one: "in {0} minute", | ||||
|             other: "in {0} minutes" | ||||
|           }, | ||||
|           past: { | ||||
|             one: "{0} minute ago", | ||||
|             other: "{0} minutes ago" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       second: { | ||||
|         displayName: "second", | ||||
|         relative: { | ||||
|           0: "now" | ||||
|         }, | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             one: "in {0} second", | ||||
|             other: "in {0} seconds" | ||||
|           }, | ||||
|           past: { | ||||
|             one: "{0} second ago", | ||||
|             other: "{0} seconds ago" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| ] | ||||
|  | ||||
| ``` | ||||
|  | ||||
| ## Notes | ||||
|  | ||||
| ### Plural Rules | ||||
|  | ||||
| The function `pluralRuleFunction()` should return the key to proper string of | ||||
| a plural form(s). The purpose of the function is to provide key of translate | ||||
| strings of correct plural form according. The different forms are described in | ||||
| [CLDR's Plural Rules][cldr-plural-rules], | ||||
|  | ||||
| [cldr-plural-rules]: http://cldr.unicode.org/index/cldr-spec/plural-rules | ||||
|  | ||||
| #### Quick Overview on CLDR Rules | ||||
|  | ||||
| Let's take English as an example. | ||||
|  | ||||
| When you describe a number, you can be either describe it as: | ||||
| * Cardinals: 1st, 2nd, 3rd ... 11th, 12th ... 21st, 22nd, 23nd .... | ||||
| * Ordinals: 1, 2, 3 ... | ||||
|  | ||||
| In any of these cases, the nouns will reflect the number with singular or plural | ||||
| form. For example: | ||||
| * in 0 days | ||||
| * in 1 day | ||||
| * in 2 days | ||||
|  | ||||
| The `pluralRuleFunction` receives 2 parameters: | ||||
| * `e`: a string representation of the number. Such as, "`1`", "`2`", "`2.1`". | ||||
| * `a`: `true` if this is "cardinal" type of description. `false` for ordinal and other case. | ||||
|  | ||||
| #### How you should write `pluralRuleFunction` | ||||
|  | ||||
| The first rule to write pluralRuleFunction is never translate the output string | ||||
| into your language. [Plural Rules][cldr-plural-rules] specified you should use | ||||
| these as the return values: | ||||
|  | ||||
|   * "`zero`" | ||||
|   * "`one`" (singular) | ||||
|   * "`two`" (dual) | ||||
|   * "`few`" (paucal) | ||||
|   * "`many`" (also used for fractions if they have a separate class) | ||||
|   * "`other`" (required—general plural form—also used if the language only has a single form) | ||||
|  | ||||
| Again, we'll use English as the example here. | ||||
|  | ||||
| Let's read the `return` statement in the pluralRuleFunction above: | ||||
| ```javascript | ||||
|   return a ? 1 == t && 11 != r ? "one" : 2 == t && 12 != r ? "two" : 3 == t && 13 != r ? "few" : "other" : 1 == e && l ? "one" : "other" | ||||
| ``` | ||||
|  | ||||
| This nested ternary is hard to read. It basically means: | ||||
| ```javascript | ||||
| // e: the number variable to examine | ||||
| // a: "true" if cardinals | ||||
| // l: "true" if the variable e has nothin after decimal mark (e.g. "1.0" would be false) | ||||
| // o: "true" if the variable e is an integer | ||||
| // t: the "ones" of the number. e.g. "3" for number "9123" | ||||
| // r: the "ones" and "tens" of the number. e.g. "23" for number "9123" | ||||
| if (a == true) { | ||||
|   if (t == 1 && r != 11) { | ||||
|     return "one"; // i.e. 1st, 21st, 101st, 121st ... | ||||
|   } else if (t == 2 && r != 12) { | ||||
|     return "two"; // i.e. 2nd, 22nd, 102nd, 122nd ... | ||||
|   } else if (t == 3 && r != 13) { | ||||
|     return "few"; // i.e. 3rd, 23rd, 103rd, 123rd ... | ||||
|   } else { | ||||
|     return "other"; // i.e. 4th, 11th, 12th, 24th ... | ||||
|   } | ||||
| } else { | ||||
|   if (e == 1 && l) { | ||||
|     return "one"; // i.e. 1 day | ||||
|   } else { | ||||
|     return "other"; // i.e. 0 days, 2 days, 3 days | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| If your language, like French, do not have complicated cardinal rules, you may | ||||
| use the French's version of it: | ||||
| ```javascript | ||||
| function (e, a) { | ||||
|   return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| If your language, like Chinese, do not have any pluralization rule at all you | ||||
| may use the Chinese's version of it: | ||||
| ```javascript | ||||
| function (e, a) { | ||||
|   return "other"; | ||||
| } | ||||
| ``` | ||||
| @@ -1,110 +0,0 @@ | ||||
| /*eslint eqeqeq: "off"*/ | ||||
| /*eslint no-nested-ternary: "off"*/ | ||||
| /*eslint quotes: "off"*/ | ||||
|  | ||||
| const rules = [{ | ||||
|   locale: "co", | ||||
|   pluralRuleFunction: function (e, a) { | ||||
|     return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; | ||||
|   }, | ||||
|   fields: { | ||||
|     year: { | ||||
|       displayName: "annu", | ||||
|       relative: { | ||||
|         0: "quist'annu", | ||||
|         1: "l'annu chì vene", | ||||
|         "-1": "l'annu passatu", | ||||
|       }, | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "in {0} annu", | ||||
|           other: "in {0} anni", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "{0} annu fà", | ||||
|           other: "{0} anni fà", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     month: { | ||||
|       displayName: "mese", | ||||
|       relative: { | ||||
|         0: "Questu mese", | ||||
|         1: "u mese chì vene", | ||||
|         "-1": "u mese passatu", | ||||
|       }, | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "in {0} mese", | ||||
|           other: "in {0} mesi", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "{0} mese fà", | ||||
|           other: "{0} mesi fà", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     day: { | ||||
|       displayName: "ghjornu", | ||||
|       relative: { | ||||
|         0: "oghje", | ||||
|         1: "dumane", | ||||
|         "-1": "eri", | ||||
|       }, | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "in {0} ghjornu", | ||||
|           other: "in {0} ghjornu", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "{0} ghjornu fà", | ||||
|           other: "{0} ghjorni fà", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     hour: { | ||||
|       displayName: "ora", | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "in {0} ora", | ||||
|           other: "in {0} ore", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "{0} ora fà", | ||||
|           other: "{0} ore fà", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     minute: { | ||||
|       displayName: "minuta", | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "in {0} minuta", | ||||
|           other: "in {0} minute", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "{0} minuta fà", | ||||
|           other: "{0} minute fà", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     second: { | ||||
|       displayName: "siconda", | ||||
|       relative: { | ||||
|         0: "avà", | ||||
|       }, | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "in {0} siconda", | ||||
|           other: "in {0} siconde", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "{0} siconda fà", | ||||
|           other: "{0} siconde fà", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }]; | ||||
|  | ||||
| export default rules; | ||||
| @@ -1,110 +0,0 @@ | ||||
| /*eslint eqeqeq: "off"*/ | ||||
| /*eslint no-nested-ternary: "off"*/ | ||||
| /*eslint quotes: "off"*/ | ||||
|  | ||||
| const rules = [{ | ||||
|   locale: "oc", | ||||
|   pluralRuleFunction: function (e, a) { | ||||
|     return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; | ||||
|   }, | ||||
|   fields: { | ||||
|     year: { | ||||
|       displayName: "an", | ||||
|       relative: { | ||||
|         0: "ongan", | ||||
|         1: "l'an que ven", | ||||
|         "-1": "l'an passat", | ||||
|       }, | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "d’aquí {0} an", | ||||
|           other: "d’aquí {0} ans", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "fa {0} an", | ||||
|           other: "fa {0} ans", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     month: { | ||||
|       displayName: "mes", | ||||
|       relative: { | ||||
|         0: "aqueste mes", | ||||
|         1: "lo mes que ven", | ||||
|         "-1": "lo mes passat", | ||||
|       }, | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "d’aquí {0} mes", | ||||
|           other: "d’aquí {0} meses", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "fa {0} mes", | ||||
|           other: "fa {0} meses", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     day: { | ||||
|       displayName: "jorn", | ||||
|       relative: { | ||||
|         0: "uèi", | ||||
|         1: "deman", | ||||
|         "-1": "ièr", | ||||
|       }, | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "d’aquí {0} jorn", | ||||
|           other: "d’aquí {0} jorns", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "fa {0} jorn", | ||||
|           other: "fa {0} jorns", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     hour: { | ||||
|       displayName: "ora", | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "d’aquí {0} ora", | ||||
|           other: "d’aquí {0} oras", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "fa {0} ora", | ||||
|           other: "fa {0} oras", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     minute: { | ||||
|       displayName: "minuta", | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "d’aquí {0} minuta", | ||||
|           other: "d’aquí {0} minutas", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "fa {0} minuta", | ||||
|           other: "fa {0} minutas", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     second: { | ||||
|       displayName: "segonda", | ||||
|       relative: { | ||||
|         0: "ara", | ||||
|       }, | ||||
|       relativeTime: { | ||||
|         future: { | ||||
|           one: "d’aquí {0} segonda", | ||||
|           other: "d’aquí {0} segondas", | ||||
|         }, | ||||
|         past: { | ||||
|           one: "fa {0} segonda", | ||||
|           other: "fa {0} segondas", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }]; | ||||
|  | ||||
| export default rules; | ||||
| @@ -1,98 +0,0 @@ | ||||
| /*eslint eqeqeq: "off"*/ | ||||
| /*eslint no-nested-ternary: "off"*/ | ||||
| /*eslint quotes: "off"*/ | ||||
| /*eslint comma-dangle: "off"*/ | ||||
|  | ||||
| const rules = [ | ||||
|   { | ||||
|     locale: "sa", | ||||
|     fields: { | ||||
|       year: { | ||||
|         displayName: "year", | ||||
|         relative: { | ||||
|           0: "this year", | ||||
|           1: "next year", | ||||
|           "-1": "last year" | ||||
|         }, | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             other: "+{0} y" | ||||
|           }, | ||||
|           past: { | ||||
|             other: "-{0} y" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       month: { | ||||
|         displayName: "month", | ||||
|         relative: { | ||||
|           0: "this month", | ||||
|           1: "next month", | ||||
|           "-1": "last month" | ||||
|         }, | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             other: "+{0} m" | ||||
|           }, | ||||
|           past: { | ||||
|             other: "-{0} m" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       day: { | ||||
|         displayName: "day", | ||||
|         relative: { | ||||
|           0: "अद्य", | ||||
|           1: "श्वः", | ||||
|           "-1": "गतदिनम्" | ||||
|         }, | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             other: "+{0} d" | ||||
|           }, | ||||
|           past: { | ||||
|             other: "-{0} d" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       hour: { | ||||
|         displayName: "hour", | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             other: "+{0} h" | ||||
|           }, | ||||
|           past: { | ||||
|             other: "-{0} h" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       minute: { | ||||
|         displayName: "minute", | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             other: "+{0} min" | ||||
|           }, | ||||
|           past: { | ||||
|             other: "-{0} min" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       second: { | ||||
|         displayName: "second", | ||||
|         relative: { | ||||
|           0: "now" | ||||
|         }, | ||||
|         relativeTime: { | ||||
|           future: { | ||||
|             other: "+{0} s" | ||||
|           }, | ||||
|           past: { | ||||
|             other: "-{0} s" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| ]; | ||||
|  | ||||
| export default rules; | ||||
| @@ -6,14 +6,14 @@ import ready from '../mastodon/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('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => { | ||||
|       return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => { | ||||
|         const root = createRoot(element); | ||||
|  | ||||
|         root.render ( | ||||
|           <AdminComponent locale={locale}> | ||||
|           <AdminComponent> | ||||
|             <Component {...componentProps} /> | ||||
|           </AdminComponent>, | ||||
|         ); | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| import './public-path'; | ||||
| import main from "mastodon/main" | ||||
|  | ||||
| import { start } from '../mastodon/common'; | ||||
| import { loadLocale } from '../mastodon/load_locale'; | ||||
| import { loadLocale } from '../mastodon/locales'; | ||||
| import { loadPolyfills } from '../mastodon/polyfills'; | ||||
|  | ||||
| start(); | ||||
|  | ||||
| loadPolyfills().then(loadLocale).then(async () => { | ||||
|   const { default: main } = await import('mastodon/main'); | ||||
|  | ||||
|   return main(); | ||||
| }).catch(e => { | ||||
|   console.error(e); | ||||
| }); | ||||
| loadPolyfills() | ||||
|   .then(loadLocale) | ||||
|   .then(main) | ||||
|   .catch(e => { | ||||
|     console.error(e); | ||||
|   }); | ||||
|   | ||||
| @@ -14,8 +14,7 @@ import { start } from '../mastodon/common'; | ||||
| import { timeAgoString }  from '../mastodon/components/relative_timestamp'; | ||||
| import emojify  from '../mastodon/features/emoji/emoji'; | ||||
| import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; | ||||
| import { loadLocale } from '../mastodon/load_locale'; | ||||
| import { getLocale }  from '../mastodon/locales'; | ||||
| import { loadLocale, getLocale } from '../mastodon/locales'; | ||||
| import { loadPolyfills } from '../mastodon/polyfills'; | ||||
| import ready from '../mastodon/ready'; | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client'; | ||||
|  | ||||
| import { start } from '../mastodon/common'; | ||||
| import ComposeContainer  from '../mastodon/containers/compose_container'; | ||||
| import { loadLocale } from '../mastodon/load_locale'; | ||||
| import { loadPolyfills } from '../mastodon/polyfills'; | ||||
| import ready from '../mastodon/ready'; | ||||
|  | ||||
| @@ -26,6 +25,6 @@ function main() { | ||||
|   ready(loaded); | ||||
| } | ||||
|  | ||||
| loadPolyfills().then(loadLocale).then(main).catch(error => { | ||||
| loadPolyfills().then(main).catch(error => { | ||||
|   console.error(error); | ||||
| }); | ||||
|   | ||||
| @@ -14,7 +14,6 @@ const config = { | ||||
|   collectCoverageFrom: [ | ||||
|     'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}', | ||||
|     '!app/javascript/mastodon/features/emoji/emoji_compressed.js', | ||||
|     '!app/javascript/mastodon/locales/locale-data/*.js', | ||||
|     '!app/javascript/mastodon/service_worker/entry.js', | ||||
|     '!app/javascript/mastodon/test_setup.js', | ||||
|   ], | ||||
|   | ||||
| @@ -50,6 +50,7 @@ | ||||
|     "abortcontroller-polyfill": "^1.7.5", | ||||
|     "atrament": "0.2.4", | ||||
|     "arrow-key-navigation": "^1.2.0", | ||||
|     "async-mutex": "^0.4.0", | ||||
|     "autoprefixer": "^10.4.14", | ||||
|     "axios": "^1.4.0", | ||||
|     "babel-loader": "^8.3.0", | ||||
|   | ||||
| @@ -33,7 +33,7 @@ describe ReactComponentHelper do | ||||
|  | ||||
|     it 'returns a tag with data attributes' do | ||||
|       expect(parsed_html.div['data-admin-component']).to eq('Name') | ||||
|       expect(parsed_html.div['data-props']).to eq('{"locale":"en","one":"two"}') | ||||
|       expect(parsed_html.div['data-props']).to eq('{"one":"two"}') | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -3273,6 +3273,13 @@ async-limiter@~1.0.0: | ||||
|   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" | ||||
|   integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== | ||||
|  | ||||
| async-mutex@^0.4.0: | ||||
|   version "0.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" | ||||
|   integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== | ||||
|   dependencies: | ||||
|     tslib "^2.4.0" | ||||
|  | ||||
| async@^2.6.2: | ||||
|   version "2.6.4" | ||||
|   resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user