From 08b237343bde17cc5d9a22b8bbaf96f2169d1958 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 14 Nov 2020 11:33:07 -0800 Subject: [PATCH] flow --- .env.sample | 3 +- app/components/InputSelect.js | 18 ++++- app/scenes/Settings/Profile.js | 7 +- flow-typed/npm/react-i18next_vx.x.x.js | 93 ++++++++++++++++++++++++++ server/api/users.js | 8 +-- server/models/User.js | 4 ++ shared/translations/de_DE.json | 4 +- shared/translations/default.json | 3 +- shared/translations/i18n.js | 28 ++++---- shared/translations/pt_PT.json | 4 +- 10 files changed, 142 insertions(+), 30 deletions(-) create mode 100644 flow-typed/npm/react-i18next_vx.x.x.js diff --git a/.env.sample b/.env.sample index 851ffd4ad6..23e78428a6 100644 --- a/.env.sample +++ b/.env.sample @@ -61,5 +61,4 @@ SMTP_REPLY_EMAIL= # Custom logo that displays on the authentication screen, scaled to height: 60px # TEAM_LOGO=https://example.com/images/logo.png -DEFAULT_LANGUAGE=en_US -AVAILABLE_LANGUAGES=["en_US", "de_DE", "pt_PT"] \ No newline at end of file +DEFAULT_LANGUAGE=en_US \ No newline at end of file diff --git a/app/components/InputSelect.js b/app/components/InputSelect.js index 5ac3c8652b..96c324a84e 100644 --- a/app/components/InputSelect.js +++ b/app/components/InputSelect.js @@ -20,11 +20,16 @@ const Select = styled.select` } `; +const Wrapper = styled.label` + max-width: ${(props) => (props.short ? "350px" : "100%")}; +`; + type Option = { label: string, value: string }; export type Props = { value?: string, label?: string, + short?: boolean, className?: string, labelHidden?: boolean, options: Option[], @@ -43,12 +48,19 @@ class InputSelect extends React.Component { }; render() { - const { label, className, labelHidden, options, ...rest } = this.props; + const { + label, + className, + labelHidden, + options, + short, + ...rest + } = this.props; const wrappedLabel = {label}; return ( - + ); } } diff --git a/app/scenes/Settings/Profile.js b/app/scenes/Settings/Profile.js index 338a970455..b3ed1cbfdd 100644 --- a/app/scenes/Settings/Profile.js +++ b/app/scenes/Settings/Profile.js @@ -11,14 +11,17 @@ import UserDelete from "scenes/UserDelete"; import Button from "components/Button"; import CenteredContent from "components/CenteredContent"; import Flex from "components/Flex"; +import HelpText from "components/HelpText"; import Input, { LabelText } from "components/Input"; import InputSelect from "components/InputSelect"; import PageTitle from "components/PageTitle"; import ImageUpload from "./components/ImageUpload"; +import { type Translate } from "types"; type Props = { auth: AuthStore, ui: UiStore, + t: Translate, }; @withTranslation() @@ -120,7 +123,6 @@ class Profile extends React.Component { required short /> -
{ onChange={this.handleLanguageChange} short /> + + {t("Please note that translations are currently in early access.")} + diff --git a/flow-typed/npm/react-i18next_vx.x.x.js b/flow-typed/npm/react-i18next_vx.x.x.js new file mode 100644 index 0000000000..7f6c2845ab --- /dev/null +++ b/flow-typed/npm/react-i18next_vx.x.x.js @@ -0,0 +1,93 @@ +// @flow +declare module "react-i18next" { + declare type TFunction = (key?: ?string, data?: ?Object) => string; + + declare type TranslatorProps = {| + t: TFunction, + i18nLoadedAt: Date, + i18n: Object, + |}; + + declare type TranslatorPropsVoid = { + t: TFunction | void, + i18nLoadedAt: Date | void, + i18n: Object | void, + }; + + declare type Translator> = ( + WrappedComponent: Component + ) => React$ComponentType< + $Diff, TranslatorPropsVoid> + >; + + declare type TranslateOptions = $Shape<{ + wait: boolean, + nsMode: "default" | "fallback", + bindi18n: false | string, + bindStore: false | string, + withRef: boolean, + translateFuncName: string, + i18n: Object, + usePureComponent: boolean, + }>; + + declare function withTranslation>( + namespaces?: | string + | Array + | (($Diff) => string | Array), + options?: TranslateOptions + ): Translator; + + declare type I18nProps = { + i18n?: Object, + ns?: string | Array, + children: (t: TFunction, { i18n: Object, t: TFunction }) => React$Node, + initialI18nStore?: Object, + initialLanguage?: string, + }; + declare var I18n: React$ComponentType; + + declare type InterpolateProps = { + className?: string, + dangerouslySetInnerHTMLPartElement?: string, + i18n?: Object, + i18nKey?: string, + options?: Object, + parent?: string, + style?: Object, + t?: TFunction, + useDangerouslySetInnerHTML?: boolean, + }; + declare var Interpolate: React$ComponentType; + + declare type TransProps = { + count?: number, + parent?: string, + i18n?: Object, + i18nKey?: string, + t?: TFunction, + }; + declare var Trans: React$ComponentType; + + declare type ProviderProps = { i18n: Object, children: React$Element<*> }; + declare var I18nextProvider: React$ComponentType; + + declare type NamespacesProps = { + components: Array>, + i18n: { loadNamespaces: Function }, + }; + declare function loadNamespaces(props: NamespacesProps): Promise; + + declare var initReactI18next: { + type: "3rdParty", + init: (instance: Object) => void, + }; + + declare function setDefaults(options: TranslateOptions): void; + + declare function getDefaults(): TranslateOptions; + + declare function getI18n(): Object; + + declare function setI18n(instance: Object): void; +} diff --git a/server/api/users.js b/server/api/users.js index 3b454c220e..1c71985819 100644 --- a/server/api/users.js +++ b/server/api/users.js @@ -66,13 +66,7 @@ router.post("users.update", auth(), async (ctx) => { if (name) user.name = name; if (avatarUrl) user.avatarUrl = avatarUrl; - if (language) { - if (process.env.AVAILABLE_LANGUAGES.includes(language)) { - user.language = language; - } else { - user.language = process.env.DEFAULT_LANGUAGE; - } - } + if (language) user.language = language; await user.save(); diff --git a/server/models/User.js b/server/models/User.js index 553b6e54d7..84c42ea367 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -4,6 +4,7 @@ import addMinutes from "date-fns/add_minutes"; import subMinutes from "date-fns/sub_minutes"; import JWT from "jsonwebtoken"; import uuid from "uuid"; +import { languages } from "../../shared/translations/i18n"; import { ValidationError } from "../errors"; import { sendEmail } from "../mailer"; import { DataTypes, sequelize, encryptedFields } from "../sequelize"; @@ -39,6 +40,9 @@ const User = sequelize.define( language: { type: DataTypes.STRING, defaultValue: process.env.DEFAULT_LANGUAGE, + validate: { + isIn: [languages], + }, }, }, { diff --git a/shared/translations/de_DE.json b/shared/translations/de_DE.json index 482c2823ff..9fdc0d562d 100644 --- a/shared/translations/de_DE.json +++ b/shared/translations/de_DE.json @@ -1,5 +1,5 @@ { - "(You)": "(Sie)", + "You": "Sie", "currently editing": "im Moment am bearbeiten", "currently viewing": "im Moment am anschauen", "viewed {{ timeAgo }} ago": "sah vor {{ timeAgo }}", @@ -143,4 +143,4 @@ "Delete account": "Konto löschen", "<0>{{collectionName}} doesn’t contain any documents yet.": "<0>{{ collectionName }} enthält noch keine Dokumente.", "Get started by creating a new one!": "Erstellen Sie jetzt ein neues!" -} +} \ No newline at end of file diff --git a/shared/translations/default.json b/shared/translations/default.json index a54a8004cf..cfba5cbd10 100644 --- a/shared/translations/default.json +++ b/shared/translations/default.json @@ -1,8 +1,8 @@ { - "(You)": "(You)", "currently editing": "currently editing", "currently viewing": "currently viewing", "viewed {{ timeAgo }} ago": "viewed {{ timeAgo }} ago", + "You": "You", "Trash": "Trash", "Archive": "Archive", "Drafts": "Drafts", @@ -138,6 +138,7 @@ "Upload": "Upload", "Full name": "Full name", "Language": "Language", + "Please note that translations are currently in early access.": "Please note that translations are currently in early access.", "Saving…": "Saving…", "Save": "Save", "Delete Account": "Delete Account", diff --git a/shared/translations/i18n.js b/shared/translations/i18n.js index af9f54cf37..3151079a38 100644 --- a/shared/translations/i18n.js +++ b/shared/translations/i18n.js @@ -6,6 +6,18 @@ import de_DE from "./de_DE.json"; import en_US from "./default.json"; import pt_PT from "./pt_PT.json"; +const resources = { + en_US: { + translation: en_US, + }, + de_DE: { + translation: de_DE, + }, + pt_PT: { + translation: pt_PT, + }, +}; + const initI18n = () => { i18n.use(initReactI18next).init({ interpolation: { @@ -20,18 +32,10 @@ const initI18n = () => { : "en_US", debug: process.env.NODE_ENV !== "production", keySeparator: false, - resources: { - en_US: { - translation: en_US, - }, - de_DE: { - translation: de_DE, - }, - pt_PT: { - translation: pt_PT, - }, - }, + resources, }); }; -export { initI18n, i18n, en_US, de_DE, pt_PT }; +const languages: string[] = Object.keys(resources); + +export { initI18n, languages, i18n, en_US, de_DE, pt_PT }; diff --git a/shared/translations/pt_PT.json b/shared/translations/pt_PT.json index 0b8eb31722..3050d54a05 100644 --- a/shared/translations/pt_PT.json +++ b/shared/translations/pt_PT.json @@ -1,5 +1,5 @@ { - "(You)": "(Tu)", + "You": "Tu", "currently editing": "neste momento a editar", "currently viewing": "neste momento a visualizar", "viewed {{ timeAgo }} ago": "viu há {{ timeAgo }}", @@ -143,4 +143,4 @@ "Delete account": "Apagar conta", "<0>{{collectionName}} doesn’t contain any documents yet.": "<0>{{ collectionName }} não contém nenhum documento ainda.", "Get started by creating a new one!": "Para começar, crie um documento novo!" -} +} \ No newline at end of file