diff --git a/src/core/build/config.ts b/src/core/build/config.ts index 07503013b..c1bd07a0b 100644 --- a/src/core/build/config.ts +++ b/src/core/build/config.ts @@ -1,5 +1,7 @@ import convict from "convict"; +import { LOCALES } from "../common/helpers/i18n/locales"; + const config = convict({ env: { doc: "The application environment.", @@ -21,6 +23,12 @@ const config = convict({ env: "DEV_PORT", arg: "dev-port", }, + defaultLocale: { + doc: "Specify the default locale to use", + format: LOCALES, + default: "en-US", + env: "LOCALE", + }, generateReport: { doc: "Generate a report using webpack-bundle-analyzer", format: Boolean, diff --git a/src/core/build/createWebpackConfig.ts b/src/core/build/createWebpackConfig.ts index 1db237c9e..4a2452400 100644 --- a/src/core/build/createWebpackConfig.ts +++ b/src/core/build/createWebpackConfig.ts @@ -92,23 +92,23 @@ export default function createWebpackConfig( const localesOptions = { pathToLocales: paths.appLocales, - // Default locale if non could be negotiated. - defaultLocale: "en-US", + // Default locale if none was specified. + defaultLocale: config.get("defaultLocale"), // Fallback locale if a translation was not found. // If not set, will use the text that is already // in the code base. - fallbackLocale: "en-US", + fallbackLocale: config.get("defaultLocale"), // Common fluent files are always included in the locale bundles. commonFiles: ["framework.ftl", "common.ftl", "ui.ftl"], // Locales that come with the main bundle. Others are loaded on demand. - bundled: ["en-US"], + bundled: [config.get("defaultLocale")], // All available locales can be loadable on demand. // To restrict available locales set: - // availableLocales: ["en-US"], + // availableLocales: [config.get("defaultLocale")], }; const additionalPlugins = [ diff --git a/src/core/build/publicPath.js b/src/core/build/publicPath.js index 3de2c5854..652893e49 100644 --- a/src/core/build/publicPath.js +++ b/src/core/build/publicPath.js @@ -1,2 +1,2 @@ __webpack_public_path__ = - JSON.parse(document.getElementById("config").innerText) || "/"; + JSON.parse(document.getElementById("config").innerText).staticURI || "/"; diff --git a/src/core/client/account/index.tsx b/src/core/client/account/index.tsx index b9fd0f04f..fb606fb19 100644 --- a/src/core/client/account/index.tsx +++ b/src/core/client/account/index.tsx @@ -13,7 +13,6 @@ async function main() { const ManagedCoralContextProvider = await createManaged({ initLocalState, localesData, - userLocales: navigator.languages, }); const Index: FunctionComponent = () => ( diff --git a/src/core/client/admin/index.tsx b/src/core/client/admin/index.tsx index b9fd0f04f..fb606fb19 100644 --- a/src/core/client/admin/index.tsx +++ b/src/core/client/admin/index.tsx @@ -13,7 +13,6 @@ async function main() { const ManagedCoralContextProvider = await createManaged({ initLocalState, localesData, - userLocales: navigator.languages, }); const Index: FunctionComponent = () => ( diff --git a/src/core/client/admin/routes/Configure/ConfigureRoute.tsx b/src/core/client/admin/routes/Configure/ConfigureRoute.tsx index e8f9acfbd..6f0db23d6 100644 --- a/src/core/client/admin/routes/Configure/ConfigureRoute.tsx +++ b/src/core/client/admin/routes/Configure/ConfigureRoute.tsx @@ -1,6 +1,7 @@ import { FormApi, FormState } from "final-form"; import React from "react"; +import { CoralContext, withContext } from "coral-framework/lib/bootstrap"; import { SubmitHookHandler } from "coral-framework/lib/form"; import { MutationProp, withMutation } from "coral-framework/lib/relay"; @@ -9,6 +10,7 @@ import NavigationWarningContainer from "./NavigationWarningContainer"; import UpdateSettingsMutation from "./UpdateSettingsMutation"; interface Props { + changeLocale: CoralContext["changeLocale"]; updateSettings: MutationProp; children: React.ReactElement; } @@ -27,6 +29,10 @@ class ConfigureRoute extends React.Component { form: FormApi ) => { await this.props.updateSettings({ settings: data }); + const localeFieldState = form.getFieldState("locale"); + if (localeFieldState && localeFieldState.dirty) { + await this.props.changeLocale(data.locale); + } form.initialize(data); }; @@ -52,6 +58,8 @@ class ConfigureRoute extends React.Component { } } -const enhanced = withMutation(UpdateSettingsMutation)(ConfigureRoute); +const enhanced = withContext(({ changeLocale }) => ({ changeLocale }))( + withMutation(UpdateSettingsMutation)(ConfigureRoute) +); export default enhanced; diff --git a/src/core/client/admin/routes/Configure/sections/General/GeneralConfig.tsx b/src/core/client/admin/routes/Configure/sections/General/GeneralConfig.tsx index ec5156681..8819802db 100644 --- a/src/core/client/admin/routes/Configure/sections/General/GeneralConfig.tsx +++ b/src/core/client/admin/routes/Configure/sections/General/GeneralConfig.tsx @@ -8,6 +8,7 @@ import ClosingCommentStreamsConfigContainer from "./ClosingCommentStreamsConfigC import CommentEditingConfigContainer from "./CommentEditingConfigContainer"; import CommentLengthConfigContainer from "./CommentLengthConfigContainer"; import GuidelinesConfigContainer from "./GuidelinesConfigContainer"; +import LocaleConfigContainer from "./LocaleConfigContainer"; import ReactionConfigContainer from "./ReactionConfigContainer"; import SitewideCommentingConfigContainer from "./SitewideCommentingConfigContainer"; @@ -19,7 +20,8 @@ interface Props { PropTypesOf["settings"] & PropTypesOf["settings"] & PropTypesOf["settings"] & - PropTypesOf["settings"]; + PropTypesOf["settings"] & + PropTypesOf["settings"]; onInitValues: (values: any) => void; } @@ -29,6 +31,11 @@ const General: FunctionComponent = ({ onInitValues, }) => ( + { const enhanced = withFragmentContainer({ settings: graphql` fragment GeneralConfigContainer_settings on Settings { + ...LocaleConfigContainer_settings ...GuidelinesConfigContainer_settings ...CommentLengthConfigContainer_settings ...CommentEditingConfigContainer_settings diff --git a/src/core/client/admin/routes/Configure/sections/General/LocaleConfigContainer.tsx b/src/core/client/admin/routes/Configure/sections/General/LocaleConfigContainer.tsx new file mode 100644 index 000000000..182db117f --- /dev/null +++ b/src/core/client/admin/routes/Configure/sections/General/LocaleConfigContainer.tsx @@ -0,0 +1,64 @@ +import { Localized } from "fluent-react/compat"; +import React, { useMemo } from "react"; +import { Field } from "react-final-form"; +import { graphql } from "react-relay"; + +import { LocaleConfigContainer_settings } from "coral-admin/__generated__/LocaleConfigContainer_settings.graphql"; +import { LocaleField } from "coral-framework/components"; +import { withFragmentContainer } from "coral-framework/lib/relay"; +import { required } from "coral-framework/lib/validation"; +import { FormField, HorizontalGutter, Typography } from "coral-ui/components"; + +import Header from "../../Header"; +import ValidationMessage from "../../ValidationMessage"; + +interface Props { + settings: LocaleConfigContainer_settings; + onInitValues: (values: LocaleConfigContainer_settings) => void; + disabled: boolean; +} + +const LocaleConfigContainer: React.FunctionComponent = props => { + useMemo(() => props.onInitValues(props.settings), [props.onInitValues]); + return ( + + + +
}> + Language +
+
+ } + > + + Choose the language for your Coral community. + + + + {({ input, meta }) => ( + <> + + + + )} + +
+
+ ); +}; + +const enhanced = withFragmentContainer({ + settings: graphql` + fragment LocaleConfigContainer_settings on Settings { + locale + } + `, +})(LocaleConfigContainer); + +export default enhanced; diff --git a/src/core/client/admin/test/configure/__snapshots__/general.spec.tsx.snap b/src/core/client/admin/test/configure/__snapshots__/general.spec.tsx.snap index 044c20724..6a99a82db 100644 --- a/src/core/client/admin/test/configure/__snapshots__/general.spec.tsx.snap +++ b/src/core/client/admin/test/configure/__snapshots__/general.spec.tsx.snap @@ -118,6 +118,81 @@ exports[`renders configure general 1`] = ` className="Box-root HorizontalGutter-root HorizontalGutter-double" data-testid="configure-generalContainer" > +
+
+ +

+ Choose the language for your Coral community. +

+ + + + + + +
+
diff --git a/src/core/client/admin/test/configure/general.spec.tsx b/src/core/client/admin/test/configure/general.spec.tsx index c2ed85321..d02a237a1 100644 --- a/src/core/client/admin/test/configure/general.spec.tsx +++ b/src/core/client/admin/test/configure/general.spec.tsx @@ -1,3 +1,5 @@ +import { SinonStub } from "sinon"; + import { ERROR_CODES } from "coral-common/errors"; import { pureMerge } from "coral-common/utils"; import { InvalidRequestError } from "coral-framework/lib/errors"; @@ -63,6 +65,48 @@ it("renders configure general", async () => { expect(within(configureContainer).toJSON()).toMatchSnapshot(); }); +it("change language", async () => { + const resolvers = createResolversStub({ + Mutation: { + updateSettings: ({ variables }) => { + expectAndFail(variables.settings.locale).toEqual("es"); + return { + settings: pureMerge(settings, variables.settings), + }; + }, + }, + }); + const { + context: { changeLocale }, + configureContainer, + generalContainer, + saveChangesButton, + } = await createTestRenderer({ resolvers }); + + const languageField = within(generalContainer).getByLabelText("Language"); + + // Let's change the language. + languageField.props.onChange("es"); + + // Send form + within(configureContainer) + .getByType("form") + .props.onSubmit(); + + // Submit button and text field should be disabled. + expect(saveChangesButton.props.disabled).toBe(true); + + // Wait for submission to be finished + await wait(() => { + expect(resolvers.Mutation!.updateSettings!.called).toBe(true); + }); + + // Wait for client to change language. + await wait(() => { + expect((changeLocale as SinonStub).called).toBe(true); + }); +}); + it("change site wide commenting", async () => { const resolvers = createResolversStub({ Mutation: { diff --git a/src/core/client/admin/test/fixtures.ts b/src/core/client/admin/test/fixtures.ts index 13f4dcdb5..f9199c641 100644 --- a/src/core/client/admin/test/fixtures.ts +++ b/src/core/client/admin/test/fixtures.ts @@ -24,6 +24,7 @@ export const settings = createFixture({ id: "settings", moderation: GQLMODERATION_MODE.POST, premodLinksEnable: false, + locale: "en-US", live: { enabled: true, configurable: true, diff --git a/src/core/client/auth/index.tsx b/src/core/client/auth/index.tsx index f8229b6d1..1ee19ff89 100644 --- a/src/core/client/auth/index.tsx +++ b/src/core/client/auth/index.tsx @@ -33,7 +33,6 @@ async function main() { const ManagedCoralContextProvider = await createManaged({ initLocalState, localesData, - userLocales: navigator.languages, }); const Index: FunctionComponent = () => ( diff --git a/src/core/client/framework/components/LocaleField.tsx b/src/core/client/framework/components/LocaleField.tsx new file mode 100644 index 000000000..ad2606982 --- /dev/null +++ b/src/core/client/framework/components/LocaleField.tsx @@ -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 { + value: LanguageCode; +} + +const LocaleField: FunctionComponent = props => { + return ( + + {Object.keys(LOCALES_MAP).map((lang: LanguageCode) => { + return ( + + ); + })} + + ); +}; + +export default LocaleField; diff --git a/src/core/client/framework/components/index.ts b/src/core/client/framework/components/index.ts index 5e878a7c3..663c93171 100644 --- a/src/core/client/framework/components/index.ts +++ b/src/core/client/framework/components/index.ts @@ -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"; diff --git a/src/core/client/framework/lib/bootstrap/CoralContext.tsx b/src/core/client/framework/lib/bootstrap/CoralContext.tsx index 7b1ceb978..83631e233 100644 --- a/src/core/client/framework/lib/bootstrap/CoralContext.tsx +++ b/src/core/client/framework/lib/bootstrap/CoralContext.tsx @@ -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; + /** Change locale and rerender */ + changeLocale: (locale: LanguageCode) => Promise; + /** Controls router transitions (for tests) */ transitionControl?: TransitionControlData; } diff --git a/src/core/client/framework/lib/bootstrap/createManaged.tsx b/src/core/client/framework/lib/bootstrap/createManaged.tsx index bfc9910ac..7bd9a2cc7 100644 --- a/src/core/client/framework/lib/bootstrap/createManaged.tsx +++ b/src/core/client/framework/lib/bootstrap/createManaged.tsx @@ -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; interface CreateContextArguments { - /** Locales that the user accepts, usually `navigator.languages`. */ - userLocales: ReadonlyArray; - /** 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 ( @@ -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 ); } diff --git a/src/core/client/framework/lib/i18n/generateBundles.ts b/src/core/client/framework/lib/i18n/generateBundles.ts index bec8f4868..ea3679e27 100644 --- a/src/core/client/framework/lib/i18n/generateBundles.ts +++ b/src/core/client/framework/lib/i18n/generateBundles.ts @@ -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, diff --git a/src/core/client/framework/lib/i18n/index.ts b/src/core/client/framework/lib/i18n/index.ts index c5d064651..a92d73751 100644 --- a/src/core/client/framework/lib/i18n/index.ts +++ b/src/core/client/framework/lib/i18n/index.ts @@ -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"; diff --git a/src/core/client/framework/lib/i18n/negotiateLanguages.ts b/src/core/client/framework/lib/i18n/negotiateLanguages.ts deleted file mode 100644 index cb5bf06d1..000000000 --- a/src/core/client/framework/lib/i18n/negotiateLanguages.ts +++ /dev/null @@ -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, - 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; -} diff --git a/src/core/client/framework/rest/install.ts b/src/core/client/framework/rest/install.ts index 28b09df04..8fee5184d 100644 --- a/src/core/client/framework/rest/install.ts +++ b/src/core/client/framework/rest/install.ts @@ -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; diff --git a/src/core/client/framework/schema/custom.ts b/src/core/client/framework/schema/custom.ts index 4d3cc85d4..c8b0ac923 100644 --- a/src/core/client/framework/schema/custom.ts +++ b/src/core/client/framework/schema/custom.ts @@ -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; export type GQLCOMMENT_STATUS_RL = RelayEnumLiteral; -export type GQLLOCALES_RL = RelayEnumLiteral; export type GQLMODERATION_MODE_RL = RelayEnumLiteral; export type GQLSTORY_STATUS_RL = RelayEnumLiteral; export type GQLUSER_AUTH_CONDITIONS_RL = RelayEnumLiteral< diff --git a/src/core/client/framework/testHelpers/createTestRenderer.tsx b/src/core/client/framework/testHelpers/createTestRenderer.tsx index 62c0dfe40..22a6600ff 100644 --- a/src/core/client/framework/testHelpers/createTestRenderer.tsx +++ b/src/core/client/framework/testHelpers/createTestRenderer.tsx @@ -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: [], diff --git a/src/core/client/install/App/InstallWizard.tsx b/src/core/client/install/App/InstallWizard.tsx index 25f884f11..47ae4c748 100644 --- a/src/core/client/install/App/InstallWizard.tsx +++ b/src/core/client/install/App/InstallWizard.tsx @@ -1,5 +1,6 @@ import React, { Component } from "react"; +import { LanguageCode } from "coral-common/helpers/i18n"; import { InstallInput } from "coral-framework/rest"; import { InstallMutation, withInstallMutation } from "./InstallMutation"; @@ -8,6 +9,7 @@ import CreateYourAccountStep from "./steps/CreateYourAccountStep"; import FinalStep from "./steps/FinalStep"; import InitialStep from "./steps/InitialStep"; import PermittedDomainsStep from "./steps/PermittedDomainsStep"; +import SelectLanguageStep from "./steps/SelectLanguageStep"; import Wizard from "./Wizard"; interface FormData { @@ -19,6 +21,7 @@ interface FormData { password: string; confirmPassword: string; allowedDomains: string[]; + locale: LanguageCode; } interface InstallWizardState { @@ -35,6 +38,7 @@ function shapeFinalData(data: FormData): InstallInput { username, password, email, + locale, } = data; return { @@ -45,6 +49,7 @@ function shapeFinalData(data: FormData): InstallInput { url: organizationURL, }, allowedDomains, + locale, }, user: { username, @@ -70,6 +75,7 @@ class InstallWizard extends Component { password: "", confirmPassword: "", allowedDomains: [], + locale: "en-US" as LanguageCode, }, }; @@ -99,6 +105,12 @@ class InstallWizard extends Component { return ( + { {currentStep !== 0 && currentStep !== wizardChildren.length - 1 && ( + + + Select Language + + Create Admin Account diff --git a/src/core/client/install/App/steps/CreateYourAccountStep.tsx b/src/core/client/install/App/steps/CreateYourAccountStep.tsx index 99d8597c0..36cfb4293 100644 --- a/src/core/client/install/App/steps/CreateYourAccountStep.tsx +++ b/src/core/client/install/App/steps/CreateYourAccountStep.tsx @@ -26,6 +26,7 @@ import { Typography, } from "coral-ui/components"; +import BackButton from "./BackButton"; import NextButton from "./NextButton"; interface FormProps { @@ -188,8 +189,12 @@ class CreateYourAccountStep extends Component { )} - + + diff --git a/src/core/client/install/App/steps/SelectLanguageStep.tsx b/src/core/client/install/App/steps/SelectLanguageStep.tsx new file mode 100644 index 000000000..fe6ba9353 --- /dev/null +++ b/src/core/client/install/App/steps/SelectLanguageStep.tsx @@ -0,0 +1,93 @@ +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent, useCallback } from "react"; +import { Field, Form } from "react-final-form"; + +import { LanguageCode } from "coral-common/helpers/i18n"; +import { LocaleField } from "coral-framework/components"; +import { useCoralContext } from "coral-framework/lib/bootstrap"; +import { OnSubmit, ValidationMessage } from "coral-framework/lib/form"; +import { required } from "coral-framework/lib/validation"; +import { + CallOut, + Flex, + FormField, + HorizontalGutter, + InputLabel, + Typography, +} from "coral-ui/components"; + +import NextButton from "./NextButton"; + +interface FormProps { + locale: string; +} + +interface Props { + onGoToNextStep: () => void; + onGoToPreviousStep: () => void; + data: FormProps; + onSaveData: (newData: FormProps) => void; +} + +const SelectLanguageStep: FunctionComponent = props => { + const { changeLocale } = useCoralContext(); + const onSubmit = useCallback>( + async (input, form) => { + props.onSaveData(input); + await changeLocale(input.locale as LanguageCode); + return props.onGoToNextStep(); + }, + [changeLocale, props.onSaveData, props.onGoToNextStep] + ); + + return ( +
+ {({ handleSubmit, submitting, submitError }) => ( + + + + + Select language for Coral + + + + + + Choose the language to be used during the installation process. + This will also be the default language for your Coral community. + + + + {submitError && ( + + {submitError} + + )} + + + {({ input, meta }) => ( + + + Language + + + + + )} + + + + + +
+ )} + + ); +}; + +export default SelectLanguageStep; diff --git a/src/core/client/install/index.tsx b/src/core/client/install/index.tsx index e46d15080..76b8c0a76 100644 --- a/src/core/client/install/index.tsx +++ b/src/core/client/install/index.tsx @@ -12,7 +12,6 @@ import "coral-ui/theme/variables.css"; async function main() { const ManagedCoralContextProvider = await createManaged({ localesData, - userLocales: navigator.languages, }); const Index: FunctionComponent = () => ( diff --git a/src/core/client/stream/index.tsx b/src/core/client/stream/index.tsx index 755d0ae73..12769072c 100644 --- a/src/core/client/stream/index.tsx +++ b/src/core/client/stream/index.tsx @@ -15,7 +15,6 @@ async function main() { const ManagedCoralContextProvider = await createManaged({ initLocalState, localesData, - userLocales: navigator.languages, pym: new PymChild({ polling: 100, }), diff --git a/src/core/client/ui/components/SelectField/SelectField.tsx b/src/core/client/ui/components/SelectField/SelectField.tsx index cc99799d6..a51fca023 100644 --- a/src/core/client/ui/components/SelectField/SelectField.tsx +++ b/src/core/client/ui/components/SelectField/SelectField.tsx @@ -54,8 +54,11 @@ const SelectField: FunctionComponent = props => { ...rest } = props; - const selectClassName = cn(classes.select, { + const rootClassName = cn(classes.root, className, { [classes.fullWidth]: fullWidth, + }); + + const selectClassName = cn(classes.select, { [classes.keyboardFocus]: keyboardFocus, }); @@ -64,7 +67,7 @@ const SelectField: FunctionComponent = props => { }); return ( - + diff --git a/src/core/client/ui/components/SelectField/__snapshots__/SelectField.spec.tsx.snap b/src/core/client/ui/components/SelectField/__snapshots__/SelectField.spec.tsx.snap index bbe32011a..480265bdb 100644 --- a/src/core/client/ui/components/SelectField/__snapshots__/SelectField.spec.tsx.snap +++ b/src/core/client/ui/components/SelectField/__snapshots__/SelectField.spec.tsx.snap @@ -2,11 +2,11 @@ exports[`renders correctly 1`] = `