Skip to content

Commit

Permalink
feat: add support for i18n (#1526)
Browse files Browse the repository at this point in the history
  • Loading branch information
harshithpabbati authored and acao committed May 16, 2020
1 parent 8d85bd2 commit 8de442a
Show file tree
Hide file tree
Showing 28 changed files with 293 additions and 47 deletions.
3 changes: 1 addition & 2 deletions examples/graphiql-webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"license": "MIT",
"description": "A GraphiQL example with webpack and typescript",
"scripts": {
"build-validate": "webpack-cli",
"build-demo": "yarn build-validate && yarn copy-demo",
"build-demo": "webpack-cli && yarn copy-demo",
"copy-demo": "mkdirp ../../packages/graphiql/webpack && copy 'dist/*' '../../packages/graphiql/webpack'",
"start": "cross-env NODE_ENV=development webpack-dev-server"
},
Expand Down
4 changes: 2 additions & 2 deletions examples/monaco-graphql-webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"typings": "src/types.d.ts",
"scripts": {
"build": "cross-env NODE_ENV=production webpack-cli",
"build-demo": "yarn build && yarn copy-demo",
"copy-demo": "mkdirp ../../packages/graphiql/monaco && copy 'bundle/*' '../../packages/graphiql/monaco'",
"build-demo-master": "yarn build && yarn copy-demo",
"copy-demo-master": "mkdirp ../../packages/graphiql/monaco && copy 'bundle/*' '../../packages/graphiql/monaco'",
"start": "cross-env NODE_ENV=development webpack-dev-server"
},
"dependencies": {
Expand Down
3 changes: 1 addition & 2 deletions examples/monaco-graphql-webpack/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
"strictPropertyInitialization": false,
"types": ["node", "jest"],
"typeRoots": ["../../node_modules/@types", "node_modules/@types"],
"lib": ["dom"],
"module": "umd"
"lib": ["dom"]
},
"references": [{ "path": "../../packages/monaco-graphql" }],
"include": ["src"],
Expand Down
7 changes: 5 additions & 2 deletions packages/graphiql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@
"codemirror": "^5.52.2",
"copy-to-clipboard": "^3.2.0",
"entities": "^2.0.0",
"markdown-it": "^10.0.0",
"monaco-graphql": "^2.3.4-alpha.4",
"graphql-languageservice": "^2.4.0-alpha.7",
"i18next": "^19.4.4",
"i18next-browser-languagedetector": "^4.1.1",
"markdown-it": "^10.0.0",
"monaco-editor": "^0.20.0",
"monaco-graphql": "^2.3.4-alpha.4",
"react-i18next": "^11.4.0",
"regenerator-runtime": "^0.13.5",
"theme-ui": "^0.3.1",
"@theme-ui/core": "^0.4.0-alpha.1"
Expand Down
1 change: 1 addition & 0 deletions packages/graphiql/resources/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ fi

babel src --ignore __tests__ --out-dir dist/
ESM=true babel src --ignore __tests__ --out-dir esm/
cp -rf src/locales dist/
echo "Bundling graphiql.js..."
browserify -g browserify-shim -s GraphiQL dist/index.js > graphiql.js
echo "Bundling graphiql.min.js..."
Expand Down
4 changes: 3 additions & 1 deletion packages/graphiql/src/components/ExecuteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, { MouseEventHandler, useState } from 'react';
import { OperationDefinitionNode } from 'graphql';
import { useSessionContext } from '../api/providers/GraphiQLSessionProvider';
import useQueryFacts from '../api/hooks/useQueryFacts';
import { useTranslation } from 'react-i18next';

/**
* ExecuteButton
Expand All @@ -30,6 +31,7 @@ export function ExecuteButton(props: ExecuteButtonProps) {
const session = useSessionContext();
const operations = queryFacts?.operations ?? [];
const hasOptions = operations && operations.length > 1;
const { t } = useTranslation('Toolbar');

let options: JSX.Element | null = null;
if (hasOptions && optionsOpen) {
Expand Down Expand Up @@ -115,7 +117,7 @@ export function ExecuteButton(props: ExecuteButtonProps) {
className="execute-button"
onMouseDown={onMouseDown}
onClick={onClick}
title="Execute Query (Ctrl-Enter)">
title={t('Execute Query (Ctrl-Enter)')}>
<svg width="34" height="34">
{pathJSX}
</svg>
Expand Down
41 changes: 23 additions & 18 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ import {
SessionContext,
} from '../api/providers/GraphiQLSessionProvider';
import { getFetcher } from '../api/common';

import { Unsubscribable, Fetcher, ReactNodeLike } from '../types';
import { Provider, useThemeLayout } from './common/themes/provider';
import Tabs from './common/Toolbar/Tabs';
import { I18nextProvider } from 'react-i18next';
import i18n from '../i18n';

const DEFAULT_DOC_EXPLORER_WIDTH = 350;

Expand Down Expand Up @@ -117,26 +120,28 @@ export type GraphiQLState = {
*/
export const GraphiQL: React.FC<GraphiQLProps> = props => {
if (!props.fetcher && !props.uri) {
throw Error('fetcher or uri property are required');
throw Error(i18n.t('Errors:Fetcher or uri property are required'));
}
const fetcher = getFetcher(props);
return (
<EditorsProvider>
<SchemaProvider
fetcher={fetcher}
config={{ uri: props.uri, ...props.schemaConfig }}>
<SessionProvider fetcher={fetcher} sessionId={0}>
<GraphiQLInternals
{...{
formatResult,
formatError,
...props,
}}>
{props.children}
</GraphiQLInternals>
</SessionProvider>
</SchemaProvider>
</EditorsProvider>
<I18nextProvider i18n={i18n}>
<EditorsProvider>
<SchemaProvider
fetcher={fetcher}
config={{ uri: props.uri, ...props.schemaConfig }}>
<SessionProvider fetcher={fetcher} sessionId={0}>
<GraphiQLInternals
{...{
formatResult,
formatError,
...props,
}}>
{props.children}
</GraphiQLInternals>
</SessionProvider>
</SchemaProvider>
</EditorsProvider>
</I18nextProvider>
);
};

Expand Down Expand Up @@ -172,7 +177,7 @@ class GraphiQLInternals extends React.Component<
graphiqlContainer: Maybe<HTMLDivElement>;
resultComponent: Maybe<typeof ResultViewer>;
variableEditorComponent: Maybe<typeof VariableEditor>;
_queryHistory: Maybe<QueryHistory>;
_queryHistory: Maybe<typeof QueryHistory>;
editorBarComponent: Maybe<HTMLDivElement>;
queryEditorComponent: Maybe<typeof QueryEditor>;
resultViewerElement: Maybe<HTMLElement>;
Expand Down
18 changes: 13 additions & 5 deletions packages/graphiql/src/components/HistoryQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import React from 'react';
import { QueryStoreItem } from '../utility/QueryStore';
import { WithTranslation, withTranslation } from 'react-i18next';

export type HandleEditLabelFn = (
query?: string,
Expand Down Expand Up @@ -38,9 +39,10 @@ export type HistoryQueryProps = {
handleToggleFavorite: HandleToggleFavoriteFn;
operationName?: string;
onSelect: HandleSelectQueryFn;
} & QueryStoreItem;
} & QueryStoreItem &
WithTranslation;

export default class HistoryQuery extends React.Component<
class HistoryQuerySource extends React.Component<
HistoryQueryProps,
{ editable: boolean }
> {
Expand All @@ -54,6 +56,7 @@ export default class HistoryQuery extends React.Component<
}

render() {
const { t } = this.props;
const displayName =
this.props.label ||
this.props.operationName ||
Expand All @@ -73,7 +76,7 @@ export default class HistoryQuery extends React.Component<
}}
onBlur={this.handleFieldBlur.bind(this)}
onKeyDown={this.handleFieldKeyDown.bind(this)}
placeholder="Type a label"
placeholder={t('Type a label')}
/>
) : (
<button
Expand All @@ -84,13 +87,15 @@ export default class HistoryQuery extends React.Component<
)}
<button
onClick={this.handleEditClick.bind(this)}
aria-label="Edit label">
aria-label={t('Edit label')}>
{'\u270e'}
</button>
<button
className={this.props.favorite ? 'favorited' : undefined}
onClick={this.handleStarClick.bind(this)}
aria-label={this.props.favorite ? 'Remove favorite' : 'Add favorite'}>
aria-label={
this.props.favorite ? t('Remove favorite') : t('Add favorite')
}>
{starIcon}
</button>
</li>
Expand Down Expand Up @@ -152,3 +157,6 @@ export default class HistoryQuery extends React.Component<
});
}
}

const HistoryQuery = withTranslation('Toolbar')(HistoryQuerySource);
export default HistoryQuery;
12 changes: 8 additions & 4 deletions packages/graphiql/src/components/QueryHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import HistoryQuery, {
HandleSelectQueryFn,
} from './HistoryQuery';
import StorageAPI from '../utility/StorageAPI';
import { WithTranslation, withTranslation } from 'react-i18next';

const MAX_QUERY_SIZE = 100000;
const MAX_HISTORY_LENGTH = 20;
Expand Down Expand Up @@ -60,13 +61,13 @@ type QueryHistoryProps = {
queryID?: number;
onSelectQuery: HandleSelectQueryFn;
storage: StorageAPI;
};
} & WithTranslation;

type QueryHistoryState = {
queries: Array<QueryStoreItem>;
};

export class QueryHistory extends React.Component<
export class QueryHistorySource extends React.Component<
QueryHistoryProps,
QueryHistoryState
> {
Expand All @@ -89,6 +90,7 @@ export class QueryHistory extends React.Component<
}

render() {
const { t } = this.props;
const queries = this.state.queries.slice().reverse();
const queryNodes = queries.map((query, i) => {
return (
Expand All @@ -102,9 +104,9 @@ export class QueryHistory extends React.Component<
);
});
return (
<section aria-label="History">
<section aria-label={t('History')}>
<div className="history-title-bar">
<div className="history-title">{'History'}</div>
<div className="history-title">{t('History')}</div>
<div className="doc-explorer-rhs">{this.props.children}</div>
</div>
<ul className="history-contents">{queryNodes}</ul>
Expand Down Expand Up @@ -183,3 +185,5 @@ export class QueryHistory extends React.Component<
});
};
}

export const QueryHistory = withTranslation('Toolbar')(QueryHistorySource);
90 changes: 90 additions & 0 deletions packages/graphiql/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';

import enTranslations from './locales/en/translation.json';
import enDocExplorer from './locales/en/DocExplorer.json';
import enToolbar from './locales/en/Toolbar.json';
import enEditor from './locales/en/Editor.json';
import enErrors from './locales/en/Errors.json';
import ruTranslations from './locales/ru/translation.json';
import ruDocExplorer from './locales/ru/DocExplorer.json';
import ruToolbar from './locales/ru/Toolbar.json';
import ruEditor from './locales/ru/Editor.json';
import ruErrors from './locales/ru/Errors.json';

const resources = {
en: {
translations: enTranslations,
DocExplorer: enDocExplorer,
Toolbar: enToolbar,
Editor: enEditor,
Errors: enErrors,
},
ru: {
translations: ruTranslations,
DocExplorer: ruDocExplorer,
Toolbar: ruToolbar,
Editor: ruEditor,
Errors: ruErrors,
},
};

i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
// Language detector options
detection: {
// order and from where user language should be detected
order: [
'querystring',
'localStorage',
'navigator',
'htmlTag',
'path',
'subdomain',
],

// keys or params to lookup language from
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
lookupFromPathIndex: 0,
lookupFromSubdomainIndex: 0,

// cache user language on
caches: ['localStorage'],
excludeCacheFor: ['cimode'], // languages to not persist (cookie, localStorage)

// optional expire and domain for set cookie
cookieMinutes: 10,
cookieDomain: window.location.hostname,

// optional htmlTag with lang attribute, the default is:
htmlTag: document.documentElement,
},

// we init with resources
resources,
fallbackLng: {
'en-US': ['en'],
default: ['en'],
},
whitelist: ['en', 'ru'],
// // have a common namespace used around the full app
// ns: ['translations'],
defaultNS: 'translation',
load: 'currentOnly',
preload: ['en', 'ru'],
keySeparator: '.', // we use content as keys
nsSeparator: ':',
interpolation: {
escapeValue: false, // not needed for react!!
},
react: {
wait: true,
},
});

export default i18n;
5 changes: 1 addition & 4 deletions packages/graphiql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// eslint-disable-next-line spaced-comment
/// <reference path='../../../node_modules/monaco-editor/monaco.d.ts'/>
// eslint-disable-next-line spaced-comment
/// <reference path='../../../packages/monaco-graphql/src/typings/monaco.d.ts'/>

import { GraphiQL } from './components/GraphiQL';
import './i18n';

export * from './api';
export * from './components/common';
Expand Down
16 changes: 16 additions & 0 deletions packages/graphiql/src/locales/en/DocExplorer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Docs": "Docs",
"Search {{name}}": "Search {{name}}...",
"Schema": "Schema",
"root types": "root types",
"Documentation Explorer": "Documentation Explorer",
"No Schema Available": "No Schema Available",
"A GraphQL schema provides a root type for each kind of operation": "A GraphQL schema provides a root type for each kind of operation.",
"query": "query",
"mutation": "mutation",
"subscription": "subscription",
"Go back to {{value}}": "Go back to {{value}}",
"Close History": "Close History",
"Open Documentation Explorer": "Open Documentation Explorer",
"Close Documentation Explorer": "Close Documentation Explorer"
}
6 changes: 6 additions & 0 deletions packages/graphiql/src/locales/en/Editor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Welcome to GraphiQL": "# Welcome to GraphiQL\\r\\n#\\r\\n# GraphiQL is an in-browser tool for writing, validating, and\\r\\n# testing GraphQL queries.\\r\\n#\\r\\n# Type queries into this side of the screen, and you will see intelligent\\r\\n# typeaheads aware of the current GraphQL type schema and live syntax and\\r\\n# validation errors highlighted within the text.\\r\\n#\\r\\n# GraphQL queries typically start with a \\\"{\\\" character. Lines that starts\\r\\n# with a # are ignored.\\r\\n#\\r\\n# An example GraphQL query might look like:\\r\\n#\\r\\n# {\\r\\n# field(arg: \\\"value\\\") {\\r\\n# subField\\r\\n# }\\r\\n# }\\r\\n#\\r\\n# Keyboard shortcuts:\\r\\n#\\r\\n# Prettify Query: Shift-Ctrl-P (or press the prettify button above)\\r\\n#\\r\\n# Merge Query: Shift-Ctrl-M (or press the merge button above)\\r\\n#\\r\\n# Run Query: Ctrl-Enter (or press the play button above)\\r\\n#\\r\\n# Auto Complete: Ctrl-Space (or just start typing)\\r\\n#",
"Automatically added leaf fields": "Automatically added leaf fields",
"Query Variables": "Query Variables",
"Query Editor": "Query Editor"
}
7 changes: 7 additions & 0 deletions packages/graphiql/src/locales/en/Errors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"Fetcher or uri property are required": "fetcher or uri property are required",
"Fetcher did not return a Promise for introspection": "Fetcher did not return a Promise for introspection.",
"Variables are invalid JSON": "Variables are invalid JSON.",
"Variables are not a JSON object": "Variables are not a JSON object.",
"no value resolved": "no value resolved"
}
Loading

0 comments on commit 8de442a

Please sign in to comment.