feat: I18n (#1653)

* feat: i18n

* Changing language single source of truth from TEAM to USER

* Changes according to @tommoor comments on PR

* Changed package.json for build:i18n and translation label

* Finished 1st MVP of i18n for outline

* new translation labels & Portuguese from Portugal translation

* Fixes from PR request

* Described language dropdown as an experimental feature

* Set keySeparator to false in order to cowork with html keys

* Added useTranslation to Breadcrumb

* Repositioned <strong> element

* Removed extra space from TemplatesMenu

* Fortified the test suite for i18n

* Fixed trans component problematic

* Check if selected language is available

* Update yarn.lock

* Removed unused Trans

* Removing debug variable from i18n init

* Removed debug variable

* test: update snapshots

* flow: Remove decorator usage to get proper flow typing
It's a shame, but hopefully we'll move to Typescript in the next 6 months and we can forget this whole Flow mistake ever happened

* translate: Drafts

* More translatable strings

* Mo translation strings

* translation: Search

* async translations loading

* cache translations in client

* Revert "cache translations in client"

This reverts commit 08fb61ce36384ff90a704faffe4761eccfb76da1.

* Revert localStorage cache for cache headers

* Update Crowdin configuration file

* Moved translation files to locales folder and fixed english text

* Added CONTRIBUTING File for CrowdIn

* chore: Move translations again to please CrowdIn

* fix: loading paths
chore: Add strings for editor

* fix: Improve validation on documents.import endpoint

* test: mock bull

* fix: Unknown mimetype should fallback to Markdown parsing if markdown extension (#1678)

* closes #1675

* Update CONTRIBUTING

* chore: Add link to translation portal from app UI

* refactor: Centralize language config

* fix: Ensure creation of i18n directory in build

* feat: Add language prompt

* chore: Improve contributing guidelines, add link from README

* chore: Normalize tab header casing

* chore: More string externalization

* fix: Language prompt in dark mode

Co-authored-by: André Glatzl <andreglatzl@gmail.com>
This commit is contained in:
Tom Moor
2020-11-29 20:04:58 -08:00
committed by GitHub
parent 63c73c9a51
commit 1285efc49a
85 changed files with 6432 additions and 2613 deletions

View File

@ -1,6 +1,7 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import UiStore from "stores/UiStore";
@ -28,60 +29,129 @@ type PropsWithRef = Props & {
history: RouterHistory,
};
class Editor extends React.Component<PropsWithRef> {
onUploadImage = async (file: File) => {
const result = await uploadFile(file, { documentId: this.props.id });
return result.url;
};
function Editor(props: PropsWithRef) {
const { id, ui, history } = props;
const { t } = useTranslation();
onClickLink = (href: string, event: MouseEvent) => {
// on page hash
if (href[0] === "#") {
window.location.href = href;
return;
}
const onUploadImage = React.useCallback(
async (file: File) => {
const result = await uploadFile(file, { documentId: id });
return result.url;
},
[id]
);
if (isInternalUrl(href) && !event.metaKey && !event.shiftKey) {
// relative
let navigateTo = href;
// probably absolute
if (href[0] !== "/") {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (err) {
navigateTo = href;
}
const onClickLink = React.useCallback(
(href: string, event: MouseEvent) => {
// on page hash
if (href[0] === "#") {
window.location.href = href;
return;
}
this.props.history.push(navigateTo);
} else if (href) {
window.open(href, "_blank");
}
};
if (isInternalUrl(href) && !event.metaKey && !event.shiftKey) {
// relative
let navigateTo = href;
onShowToast = (message: string) => {
if (this.props.ui) {
this.props.ui.showToast(message);
}
};
// probably absolute
if (href[0] !== "/") {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (err) {
navigateTo = href;
}
}
render() {
return (
<ErrorBoundary reloadOnChunkMissing>
<StyledEditor
ref={this.props.forwardedRef}
uploadImage={this.onUploadImage}
onClickLink={this.onClickLink}
onShowToast={this.onShowToast}
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
tooltip={EditorTooltip}
{...this.props}
/>
</ErrorBoundary>
);
}
history.push(navigateTo);
} else if (href) {
window.open(href, "_blank");
}
},
[history]
);
const onShowToast = React.useCallback(
(message: string) => {
if (ui) {
ui.showToast(message);
}
},
[ui]
);
const dictionary = React.useMemo(() => {
return {
addColumnAfter: t("Insert column after"),
addColumnBefore: t("Insert column before"),
addRowAfter: t("Insert row after"),
addRowBefore: t("Insert row before"),
alignCenter: t("Align center"),
alignLeft: t("Align left"),
alignRight: t("Align right"),
bulletList: t("Bulleted list"),
checkboxList: t("Todo list"),
codeBlock: t("Code block"),
codeCopied: t("Copied to clipboard"),
codeInline: t("Code"),
createLink: t("Create link"),
createLinkError: t("Sorry, an error occurred creating the link"),
createNewDoc: t("Create a new doc"),
deleteColumn: t("Delete column"),
deleteRow: t("Delete row"),
deleteTable: t("Delete table"),
em: t("Italic"),
embedInvalidLink: t("Sorry, that link wont work for this embed type"),
findOrCreateDoc: t("Find or create a doc…"),
h1: t("Big heading"),
h2: t("Medium heading"),
h3: t("Small heading"),
heading: t("Heading"),
hr: t("Divider"),
image: t("Image"),
imageUploadError: t("Sorry, an error occurred uploading the image"),
info: t("Info"),
infoNotice: t("Info notice"),
link: t("Link"),
linkCopied: t("Link copied to clipboard"),
mark: t("Highlight"),
newLineEmpty: t("Type '/' to insert…"),
newLineWithSlash: t("Keep typing to filter…"),
noResults: t("No results"),
openLink: t("Open link"),
orderedList: t("Ordered list"),
pasteLink: t("Paste a link…"),
pasteLinkWithTitle: (service: string) =>
t("Paste a {{service}} link…", { service }),
placeholder: t("Placeholder"),
quote: t("Quote"),
removeLink: t("Remove link"),
searchOrPasteLink: t("Search or paste a link…"),
strikethrough: t("Strikethrough"),
strong: t("Bold"),
subheading: t("Subheading"),
table: t("Table"),
tip: t("Tip"),
tipNotice: t("Tip notice"),
warning: t("Warning"),
warningNotice: t("Warning notice"),
};
}, [t]);
return (
<ErrorBoundary reloadOnChunkMissing>
<StyledEditor
ref={props.forwardedRef}
uploadImage={onUploadImage}
onClickLink={onClickLink}
onShowToast={onShowToast}
embeds={props.disableEmbeds ? EMPTY_ARRAY : embeds}
tooltip={EditorTooltip}
dictionary={dictionary}
{...props}
/>
</ErrorBoundary>
);
}
const StyledEditor = styled(RichMarkdownEditor)`