[CORL-406] Tenant Locale Selection (#2450)

* feat: added preload config

* feat: support changing locale

* fix: name case

* fix: removed unused code

* feat: added translations for default reactions

* fix: do not translate icon

* fix: shorter i18n keys
This commit is contained in:
Wyatt Johnson
2019-09-05 17:02:06 +00:00
committed by GitHub
parent 5bf4f22931
commit 04c56b3fb5
54 changed files with 673 additions and 207 deletions
@@ -0,0 +1,25 @@
import React, { FunctionComponent } from "react";
import { LanguageCode, LOCALES_MAP } from "coral-common/helpers/i18n";
import { PropTypesOf } from "coral-framework/types";
import { Option, SelectField } from "coral-ui/components";
interface Props extends PropTypesOf<typeof SelectField> {
value: LanguageCode;
}
const LocaleField: FunctionComponent<Props> = props => {
return (
<SelectField {...props}>
{Object.keys(LOCALES_MAP).map((lang: LanguageCode) => {
return (
<Option value={lang} key={lang}>
{LOCALES_MAP[lang]}
</Option>
);
})}
</SelectField>
);
};
export default LocaleField;
@@ -6,3 +6,4 @@ export { default as OIDCButton } from "./OIDCButton";
export { default as Markdown } from "./Markdown";
export { default as FadeInTransition } from "./FadeInTransition";
export { default as DurationField, DURATION_UNIT } from "./DurationField";
export { default as LocaleField } from "./LocaleField";
@@ -7,6 +7,7 @@ import { MediaQueryMatchers } from "react-responsive";
import { Formatter } from "react-timeago";
import { Environment } from "relay-runtime";
import { LanguageCode } from "coral-common/helpers/i18n";
import { BrowserInfo } from "coral-framework/lib/browserInfo";
import { PostMessageService } from "coral-framework/lib/postMessage";
import { RestClient } from "coral-framework/lib/rest";
@@ -64,6 +65,9 @@ export interface CoralContext {
/** Clear session data. */
clearSession: (nextAccessToken?: string | null) => Promise<void>;
/** Change locale and rerender */
changeLocale: (locale: LanguageCode) => Promise<void>;
/** Controls router transitions (for tests) */
transitionControl?: TransitionControlData;
}
@@ -7,12 +7,14 @@ import { Formatter } from "react-timeago";
import { Environment, RecordSource, Store } from "relay-runtime";
import uuid from "uuid/v1";
import { LanguageCode } from "coral-common/helpers/i18n";
import { getBrowserInfo } from "coral-framework/lib/browserInfo";
import {
commitLocalUpdatePromisified,
LOCAL_ID,
setAccessTokenInLocalState,
} from "coral-framework/lib/relay";
import { RestClient } from "coral-framework/lib/rest";
import {
createLocalStorage,
createPromisifiedStorage,
@@ -20,11 +22,9 @@ import {
createSessionStorage,
PromisifiedStorage,
} from "coral-framework/lib/storage";
import { RestClient } from "coral-framework/lib/rest";
import { ClickFarAwayRegister } from "coral-ui/components/ClickOutside";
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
import { generateBundles, LocalesData } from "../i18n";
import {
createManagedSubscriptionClient,
createNetwork,
@@ -41,9 +41,6 @@ export type InitLocalState = (
) => void | Promise<void>;
interface CreateContextArguments {
/** Locales that the user accepts, usually `navigator.languages`. */
userLocales: ReadonlyArray<string>;
/** Locales data that is returned by our `locales-loader`. */
localesData: LocalesData;
@@ -128,11 +125,12 @@ function createRestClient(tokenGetter: () => string, clientID: string) {
* Returns a managed CoralContextProvider, that includes given context
* and handles context changes, e.g. when a user session changes.
*/
function createMangedCoralContextProvider(
function createManagedCoralContextProvider(
context: CoralContext,
subscriptionClient: ManagedSubscriptionClient,
clientID: string,
initLocalState: InitLocalState
initLocalState: InitLocalState,
localesData: LocalesData
) {
const ManagedCoralContextProvider = class extends Component<
{},
@@ -144,6 +142,7 @@ function createMangedCoralContextProvider(
context: {
...context,
clearSession: this.clearSession,
changeLocale: this.changeLocale,
},
};
}
@@ -196,6 +195,25 @@ function createMangedCoralContextProvider(
);
};
// This is called when the locale should change.
private changeLocale = async (locale: LanguageCode) => {
// Add fallback locale.
const locales = [localesData.fallbackLocale];
if (locale && locale !== localesData.fallbackLocale) {
locales.splice(0, 0, locale);
}
const localeBundles = await generateBundles(locales, localesData);
const newContext = {
...this.state.context,
locales,
localeBundles,
};
// Propagate new context.
this.setState({
context: newContext,
});
};
public render() {
return (
<CoralContextProvider value={this.state.context}>
@@ -240,7 +258,6 @@ function resolveSessionStorage(pym?: PymChild): PromisifiedStorage {
*/
export default async function createManaged({
initLocalState = noop,
userLocales,
localesData,
pym,
eventEmitter = new EventEmitter2({ wildcard: true, maxListeners: 20 }),
@@ -261,11 +278,24 @@ export default async function createManaged({
}
// Initialize i18n.
const locales = negotiateLanguages(userLocales, localesData);
const locales = [localesData.fallbackLocale];
if (
document.documentElement.lang &&
document.documentElement.lang !== localesData.fallbackLocale
) {
// Use locale specified by the server.
locales.splice(0, 0, document.documentElement.lang);
} else if (
localesData.defaultLocale &&
localesData.defaultLocale !== localesData.fallbackLocale
) {
// Use default locale.
locales.splice(0, 0, localesData.defaultLocale);
}
if (process.env.NODE_ENV !== "production") {
// tslint:disable:next-line: no-console
console.log(`Negotiated locales ${JSON.stringify(locales)}`);
console.log(`Using locales ${JSON.stringify(locales)}`);
}
const localeBundles = await generateBundles(locales, localesData);
@@ -304,6 +334,9 @@ export default async function createManaged({
// Noop, this is later replaced by the
// managed CoralContextProvider.
clearSession: (nextAccessToken?: string | null) => Promise.resolve(),
// Noop, this is later replaced by the
// managed CoralContextProvider.
changeLocale: (locale?: LanguageCode) => Promise.resolve(),
};
// Initialize local state.
@@ -317,10 +350,11 @@ export default async function createManaged({
// Returns a managed CoralContextProvider, that includes the above
// context and handles context changes, e.g. when a user session changes.
return createMangedCoralContextProvider(
return createManagedCoralContextProvider(
context,
subscriptionClient,
clientID,
initLocalState
initLocalState,
localesData
);
}
@@ -36,8 +36,6 @@ if (process.env.NODE_ENV !== "production") {
* Given a locales array and the `data` from the `locales-loader`,
* generateMessages returns an Array of MessageContext as a Promise.
* This array is meant to be consumed by `react-fluent`.
*
* Use it in conjunction with `negotiateLanguages`.
*/
export default async function generateBundles(
locales: ReadonlyArray<string>,
@@ -1,5 +1,4 @@
export { default as generateBundles } from "./generateBundles";
export { default as negotiateLanguages } from "./negotiateLanguages";
export { BundledLocales, LoadableLocales, LocalesData } from "./locales";
export { default as getMessage } from "./getMessage";
export { default as withGetMessage, GetMessage } from "./withGetMessage";
@@ -1,27 +0,0 @@
import { negotiateLanguages as negotiate } from "fluent-langneg/compat";
import { LocalesData } from "./locales";
/**
* negotiateLanguages accepts `userLocales` which usually comes from
* `navigator.languages` and the locales `data` as generated by
* the `locales-loader` and returns an array of matching languages.
*/
export default function negotiateLanguages(
userLocales: ReadonlyArray<string>,
data: LocalesData
) {
// Choose locale that is best for the user.
const languages = negotiate(userLocales, data.availableLocales, {
defaultLocale: data.defaultLocale,
strategy: "lookup",
});
if (data.fallbackLocale && languages[0] !== data.fallbackLocale) {
// Use default locale as fallback in case we have
// missing keys.
languages.push(data.fallbackLocale);
}
return languages;
}
@@ -1,3 +1,5 @@
import { LanguageCode } from "coral-common/helpers/i18n";
import { RestClient } from "../lib/rest";
export interface InstallInput {
@@ -8,6 +10,7 @@ export interface InstallInput {
url: string;
};
allowedDomains: string[];
locale: LanguageCode;
};
user: {
username: string;
@@ -10,7 +10,6 @@ import {
GQLCOMMENT_FLAG_REPORTED_REASON,
GQLCOMMENT_SORT,
GQLCOMMENT_STATUS,
GQLLOCALES,
GQLMODERATION_MODE,
GQLMODERATION_QUEUE,
GQLSTORY_STATUS,
@@ -35,7 +34,6 @@ export type GQLCOMMENT_FLAG_REPORTED_REASON_RL = RelayEnumLiteral<
>;
export type GQLCOMMENT_SORT_RL = RelayEnumLiteral<typeof GQLCOMMENT_SORT>;
export type GQLCOMMENT_STATUS_RL = RelayEnumLiteral<typeof GQLCOMMENT_STATUS>;
export type GQLLOCALES_RL = RelayEnumLiteral<typeof GQLLOCALES>;
export type GQLMODERATION_MODE_RL = RelayEnumLiteral<typeof GQLMODERATION_MODE>;
export type GQLSTORY_STATUS_RL = RelayEnumLiteral<typeof GQLSTORY_STATUS>;
export type GQLUSER_AUTH_CONDITIONS_RL = RelayEnumLiteral<
@@ -104,6 +104,7 @@ export default function createTestRenderer<
uuidGenerator: createUUIDGenerator(),
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
clearSession: sinon.stub(),
changeLocale: sinon.stub(),
transitionControl: {
allowTransition: true,
history: [],