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.local
commit
734e186717
25 changed files with 152 additions and 617 deletions
@ -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 }); |
|
||||||
} |
|
@ -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'; |
|
@ -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'; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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,14 +1,15 @@ |
|||||||
import './public-path'; |
import './public-path'; |
||||||
|
import main from "mastodon/main" |
||||||
|
|
||||||
import { start } from '../mastodon/common'; |
import { start } from '../mastodon/common'; |
||||||
import { loadLocale } from '../mastodon/load_locale'; |
import { loadLocale } from '../mastodon/locales'; |
||||||
import { loadPolyfills } from '../mastodon/polyfills'; |
import { loadPolyfills } from '../mastodon/polyfills'; |
||||||
|
|
||||||
start(); |
start(); |
||||||
|
|
||||||
loadPolyfills().then(loadLocale).then(async () => { |
loadPolyfills() |
||||||
const { default: main } = await import('mastodon/main'); |
.then(loadLocale) |
||||||
|
.then(main) |
||||||
return main(); |
.catch(e => { |
||||||
}).catch(e => { |
console.error(e); |
||||||
console.error(e); |
}); |
||||||
}); |
|
||||||
|
Loading…
Reference in new issue