diff --git a/makeMessages.sh b/makeMessages.sh new file mode 100755 index 0000000..bf114ff --- /dev/null +++ b/makeMessages.sh @@ -0,0 +1,34 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess + +commands = [ + "rm -rf src/app/l10n.bak", + "cp -r src/app/l10n src/app/l10n.bak", + "rm -rf trans-tmp extracted-messages", + "node_modules/.bin/tsc -p . --target ES6 --module es6 --jsx preserve --outDir trans-tmp", + "node_modules/.bin/babel --plugins react-intl \"trans-tmp/**/*.jsx\"", +] + +progress = "" + +for command in commands: + progress += "." + sys.stdout.write('%s\r' % (progress)), + sys.stdout.flush() + + proc = subprocess.Popen([ command ], stdout=subprocess.PIPE, shell=True, stderr=subprocess.STDOUT,) + (out, err) = proc.communicate() + + if "SyntaxError:" in out: + print "SyntaxError occured. Stopping operation.\n" + print out + sys.exit(1) + break + +subprocess.Popen([ "npm run manage:translations" ], shell=True).wait() + +sys.exit(0) diff --git a/package-lock.json b/package-lock.json index c1f7a4a..e5a97f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5410,7 +5410,7 @@ "color": "2.0.0", "lodash": "4.17.4", "tslint": "5.7.0", - "typescript": "2.5.3" + "typescript": "2.6.1" }, "dependencies": { "@types/color": { @@ -6527,9 +6527,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", - "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", + "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" }, "ua-parser-js": { "version": "0.7.14", diff --git a/package.json b/package.json index 5f265ce..ff7805e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "babel-jest": "^20.0.3", "concurrently": "^3.4.0", "tslint": "^5.7.0", - "typescript": "^2.5.3" + "typescript": "^2.6.1" }, "jest": { "preset": "react-native" diff --git a/src/index.tsx b/src/index.tsx index bd582ea..54b37b2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,401 +1,22 @@ -// import * as React from 'react'; -// import * as React from 'react' -// import * as RN from 'react-native' -// import * as t from 'io-ts' -// import * as H from 'history' -// // import * as ReactRouter from 'react-router' -// import * as ReactRouterNative from 'react-router-native' -// import * as ReactRouter from 'react-router' -// import * as moment from 'moment' - -// import TrainingListScreen from './screens/TrainingListScreen' -// import TrainingScreen from './screens/TrainingScreen' -// import * as Util from './Utils' -// import * as Model from './Models' -// import * as BS from 'react-native-better-styles' -// const { s, colors } = BS - -// const { width } = RN.Dimensions.get('window') - -// const palette: BS.Palette = { -// greyDarkest: '#2e333d', -// greyDarker: '#434b55', -// greyDark: '#555b65', -// grey: '#8a949d', -// greyLight: '#d2dadd', -// greyLighter: '#e5eaee', -// greyLightest: '#fafafa', -// white: '#ffffff', -// black: '#000000', -// blueDark: '#2b55e4', -// blue: '#2c5cff', -// blueLight: '#587eff', -// red: '#ff2b71', -// orange: '#ff605e', -// yellow: '#fbcf00', -// green: '#0cddae', -// t: 'rgba(0,0,0,0)' -// } - -// const headings: BS.Multipliers = { -// '7': 0.75, -// '6': 0.85, -// '5': 1, -// '4': 1.2, -// '3': 1.6, -// '2': 2, -// '1': 3.25 -// } - -// BS.build( -// { -// remSize: 15, -// palette, -// headings -// } as BS.Options -// ) - -// interface AppState { -// editingTrainingIndex: number | null -// currentTraining: Model.OngoingTraining | Model.NotStartedTraining | Model.FinishedTraining | null -// finishedTrainings: Model.FinishedTraining[] -// animationCallback?: () => void -// } - -// type AppProps = ReactRouter.RouteComponentProps<{}> - -// class App extends React.Component { -// constructor(props: AppProps) { -// super(props) -// this.state = { -// editingTrainingIndex: null, -// currentTraining: null, -// finishedTrainings: [] -// } -// } - -// async componentDidMount() { -// await this.fetchData() -// if (!!this.state.currentTraining) this.props.history.push('/training') -// } - -// componentDidUpdate() { -// this.storeData() -// } - -// fetchData = async () => { -// try { -// const storedStateJSON = await RN.AsyncStorage.getItem('@Gymple:State') -// if (storedStateJSON !== null) { -// const state = await Util.decode( -// JSON.parse(storedStateJSON), -// t.interface({ -// currentTraining: t.union([ -// Model.TOngoingTraining, -// Model.TNotStartedTraining, -// Model.TFinishedTraining, -// t.null -// ]), -// finishedTrainings: t.array(Model.TFinishedTraining) -// }) -// ) -// this.setState(state) -// } -// } catch (error) { -// await RN.AsyncStorage.setItem('@Gymple:State', '') -// throw new Error(error) -// } -// } - -// storeData = async () => { -// const { currentTraining, finishedTrainings } = this.state -// try { -// await RN.AsyncStorage.setItem( -// '@Gymple:State', -// JSON.stringify({ -// currentTraining, -// finishedTrainings -// }) -// ) -// } catch (error) { -// throw new Error(error) -// } -// } - -// startNewTraining = () => { -// this.setState({ -// currentTraining: { -// kind: 'NotStartedTraining', -// title: 'Training on ' + moment().format('dddd'), -// plannedExercises: [] -// }, -// editingTrainingIndex: null -// }) -// } - -// restartFinishedTraining = (finishedTraining: Model.FinishedTraining) => { -// const notStartedTrainingFromFinished: Model.NotStartedTraining = { -// kind: 'NotStartedTraining', -// title: finishedTraining.title + ' copy', -// plannedExercises: finishedTraining.completedExercises -// } -// this.setState({ -// currentTraining: notStartedTrainingFromFinished, -// editingTrainingIndex: null -// }) -// } - -// updateCurrentTraining = ( -// currentTraining: Model.OngoingTraining | Model.NotStartedTraining | Model.FinishedTraining, -// editingTrainingIndex: number | null -// ) => { -// this.setState({ -// currentTraining, -// editingTrainingIndex -// }) -// } - -// finishCurrentTraining = () => { -// const { currentTraining, finishedTrainings, editingTrainingIndex } = this.state -// if (currentTraining) { -// switch (currentTraining.kind) { -// case 'OngoingTraining': -// const newFinishedTraining: Model.FinishedTraining = { -// kind: 'FinishedTraining', -// title: currentTraining.title, -// startedAt: currentTraining.startedAt, -// finishedAt: new Date(), -// completedExercises: currentTraining.completedExercises -// } -// this.setState({ -// currentTraining: null, -// finishedTrainings: -// newFinishedTraining.completedExercises.length > 0 -// ? [newFinishedTraining, ...finishedTrainings] -// : finishedTrainings -// }) -// break -// case 'FinishedTraining': -// this.setState({ -// currentTraining: null, -// editingTrainingIndex: null, -// finishedTrainings: -// currentTraining.completedExercises.length > 0 -// ? finishedTrainings.map((t, i) => (i === editingTrainingIndex ? currentTraining : t)) -// : finishedTrainings.filter((_, i) => i !== editingTrainingIndex) -// }) -// break -// case 'NotStartedTraining': -// this.setState({ currentTraining: null }) -// break -// default: -// Util.shouldNeverHappen(currentTraining) -// } -// } -// } - -// removeFinishedTraining = (index: number) => { -// const { finishedTrainings } = this.state -// this.setState({ -// finishedTrainings: finishedTrainings.filter((_, i) => i !== index) -// }) -// } - -// render() { -// const { history, location } = this.props -// const { currentTraining, finishedTrainings, editingTrainingIndex, animationCallback } = this.state - -// const uniqeCompletedExercises = finishedTrainings.reduce((completedExercises, training) => { -// const completedExercisesTitles: string[] = completedExercises.map(e => e.title) -// return [ -// ...completedExercises, -// ...training.completedExercises.filter(e => completedExercisesTitles.indexOf(e.title) === -1) -// ] as Model.Exercise[] -// }, [] as Model.Exercise[]) - -// return ( -// -// -// -// { -// this.startNewTraining() -// history.push('/training') -// }} -// onOpenTraining={(training, i) => { -// this.updateCurrentTraining(training, i) -// history.push('/training') -// }} -// onRemoveFinishedTraining={this.removeFinishedTraining} -// />} -// /> -// -// currentTraining -// ? { -// this.setState( -// { -// animationCallback: () => { -// this.finishCurrentTraining() -// this.setState({ animationCallback: undefined }) -// } -// }, -// () => history.goBack() -// ) -// }} -// onRestartFinished={this.restartFinishedTraining} -// onUpdate={training => this.updateCurrentTraining(training, editingTrainingIndex)} -// /> -// : } -// /> -// -// -// ) -// } -// } - -// interface AnimatedChildRouteProps { -// location: H.Location -// history: H.History -// children: React.ReactNode -// animationCallback?: () => void -// } - -// interface AnimatedChildRouteState { -// prevChildren: React.ReactNode | null -// anim: RN.Animated.Value -// } - -// class AnimatedChildRoute extends React.Component { -// constructor(props: AnimatedChildRouteProps) { -// super(props) -// this.state = { -// anim: new RN.Animated.Value(100), -// prevChildren: null -// } -// } - -// componentWillReceiveProps(nextProps: AnimatedChildRouteProps) { -// const { animationCallback } = this.props -// if (this.props.location.pathname !== nextProps.location.pathname) { -// this.state.anim.setValue(0) -// this.setState( -// { -// prevChildren: this.props.children -// }, -// () => { -// RN.Animated -// .timing(this.state.anim, { -// easing: RN.Easing.quad, -// toValue: 100, -// duration: 300 -// }) -// .start(() => { -// this.setState( -// { -// prevChildren: null -// }, -// () => !!animationCallback && animationCallback() -// ) -// }) -// } -// ) -// } -// } - -// render() { -// const { prevChildren, anim } = this.state -// const { children, history } = this.props - -// if (history.action === 'POP') -// return ( -// -// -// {children} -// -// {prevChildren && -// -// {prevChildren} -// } -// -// ) -// return ( -// -// {prevChildren && -// -// {prevChildren} -// } -// -// {children} -// -// -// ) -// } -// } - -// const Routed = ReactRouter.withRouter(App) as any -// class RoutedApp extends React.Component { -// render() { -// return ( -// -// -// -// ) -// } -// } - import * as React from 'react' import * as RN from 'react-native' import * as BS from 'react-native-better-styles' import * as ReactRouterNative from 'react-router-native' import * as MobxReact from 'mobx-react/native' +import * as ReactIntl from 'react-intl' + +import strings from './localizations' +import Trans from './translation' + +import 'intl' +const en = require('react-intl/locale-data/en') +const ru = require('react-intl/locale-data/ru') + +Trans.loadTranslations(strings) +ReactIntl.addLocaleData([...en, ...ru]) + +window['trans'] = Trans import { stores } from './store/index' import { palette, multipliers, headings, fonts } from './stylesSettings' @@ -417,6 +38,15 @@ function calculateRemSize(width: number): number { return 13 } +function getLocale() { + if (RN.Platform.OS === 'android') { + return RN.NativeModules.I18nManager.localeIdentifier.substring(0, 2) + } else if (RN.Platform.OS === 'ios') { + return RN.NativeModules.SettingsManager.settings.AppleLocale.substring(0, 2) + } + return 'en' +} + // building better-styles BS.build({ remSize: calculateRemSize(width > height ? height : width), @@ -433,39 +63,29 @@ stores.dataStore.generateInitialData() class App extends React.Component<{}> { render() { return ( - - - - - } /> - - - - - - {/* + + + + + + } /> + + + + + + {/* */} - - - - + + + + + ) } } RN.AppRegistry.registerComponent('Gymple', () => App) -// import * as ReactIntl from 'react-intl'; -// import 'intl'; - -// import localizations from './localizations'; - -/* -const en = require('react-intl/locale-data/en'); -const ru = require('react-intl/locale-data/ru'); - -ReactIntl.addLocaleData([...en, ...ru]); - -*/ diff --git a/src/localizations.ts b/src/localizations.ts index 5cb3670..0321b4e 100644 --- a/src/localizations.ts +++ b/src/localizations.ts @@ -1,8 +1,10 @@ export default { en: { - 'test': 'test', + test: 'test', + startNew: 'Start Nu' }, ru: { - 'test': 'тест', - }, -}; + test: 'тест', + startNew: 'Новая' + } +} diff --git a/src/screens/TrainingsListScreen.tsx b/src/screens/TrainingsListScreen.tsx index 5b2f942..aadf58f 100644 --- a/src/screens/TrainingsListScreen.tsx +++ b/src/screens/TrainingsListScreen.tsx @@ -13,17 +13,29 @@ import { shadows } from '../stylesSettings' // import Popup from '../components/Popup' import Navbar from '../components/Navbar' import { stores } from '../store' -import { FinishedTraining } from '../store/dataStore' -// import * as Util from '../utils' +import { FinishedTraining, Exercise, Muscle } from '../store/dataStore' +import * as ReactIntl from 'react-intl' +import Trans from '../translation' + +const messages = Trans.defineMessages({ + startNew: { + id: 'startNew', + defaultMessage: 'Start New' + }, + fuckYou: { + id: 'fuckYou', + defaultMessage: 'Fuck you' + } +}) type TrainingsListScreenProps = { dataStore: typeof stores.dataStore routing: typeof stores.routing -} +} & ReactIntl.InjectedIntlProps @MobxReact.inject('dataStore', 'routing') @MobxReact.observer -export default class TrainingsListScreen extends React.Component { +class TrainingsListScreen extends React.Component { createNewTraining = () => { const { dataStore, routing } = this.props const finishedTraining = new FinishedTraining() @@ -33,7 +45,7 @@ export default class TrainingsListScreen extends React.ComponentStart new} + rightBtn={ + + {intl.formatMessage(messages.startNew)} + + } /> @@ -95,6 +111,8 @@ export default class TrainingsListScreen extends React.Component void onPress: () => void @@ -104,6 +122,9 @@ type FinishedTrainingViewProps = { @MobxReact.observer class FinishedTrainingView extends React.Component { + constructor() { + super() + } private animatedValue: RN.Animated.Value = new RN.Animated.Value(0) componentDidMount() { @@ -123,10 +144,10 @@ class FinishedTrainingView extends React.Component { render() { const { training, onPress } = this.props - const exercises = training.completedSets.reduce((acc, set) => [...acc, ...set.exercises], []) + const exercises = training.completedSets.reduce((acc, set) => [...acc, ...set.exercises], [] as Exercise[]) const duration = moment(training.finishedAt).diff(moment(training.startedAt), 'minutes') - const affectedMuscles = exercises.reduce((acc, exercise) => [...acc, ...exercise.primaryMuscles], []) + const affectedMuscles = exercises.reduce((acc, exercise) => [...acc, ...exercise.primaryMuscles], [] as Muscle[]) const muscleUsing: { [key: string]: number } = affectedMuscles.map(m => m.title).reduce(function(acc, curr) { if (typeof acc[curr] == 'undefined') { diff --git a/src/store/dataStore.ts b/src/store/dataStore.ts index b2bd224..40071f2 100644 --- a/src/store/dataStore.ts +++ b/src/store/dataStore.ts @@ -350,20 +350,26 @@ export class ExerciseTemplate { @Mobx.action updatePrimaryMusclesByIds(ids: string[]) { this.replacePrimaryMuscles( - ids.reduce((acc, id) => { - const relatedMuscle = stores.dataStore.muscles.find(m => m.id === id) - return relatedMuscle ? [...acc, relatedMuscle] : acc - }, []) + ids.reduce( + (acc, id) => { + const relatedMuscle = stores.dataStore.muscles.find(m => m.id === id) + return relatedMuscle ? [...acc, relatedMuscle] : acc + }, + [] as Muscle[] + ) ) } @Mobx.action updateSecondaryMusclesByIds(ids: string[]) { this.replaceSecondaryMuscles( - ids.reduce((acc, id) => { - const relatedMuscle = stores.dataStore.muscles.find(m => m.id === id) - return relatedMuscle ? [...acc, relatedMuscle] : acc - }, []) + ids.reduce( + (acc, id) => { + const relatedMuscle = stores.dataStore.muscles.find(m => m.id === id) + return relatedMuscle ? [...acc, relatedMuscle] : acc + }, + [] as Muscle[] + ) ) } @@ -469,20 +475,26 @@ export class Exercise { @Mobx.action updatePrimaryMusclesByIds(ids: string[]) { this.replacePrimaryMuscles( - ids.reduce((acc, id) => { - const relatedMuscle = stores.dataStore.muscles.find(m => m.id === id) - return relatedMuscle ? [...acc, relatedMuscle] : acc - }, []) + ids.reduce( + (acc, id) => { + const relatedMuscle = stores.dataStore.muscles.find(m => m.id === id) + return relatedMuscle ? [...acc, relatedMuscle] : acc + }, + [] as Muscle[] + ) ) } @Mobx.action updateSecondaryMusclesByIds(ids: string[]) { this.replaceSecondaryMuscles( - ids.reduce((acc, id) => { - const relatedMuscle = stores.dataStore.muscles.find(m => m.id === id) - return relatedMuscle ? [...acc, relatedMuscle] : acc - }, []) + ids.reduce( + (acc, id) => { + const relatedMuscle = stores.dataStore.muscles.find(m => m.id === id) + return relatedMuscle ? [...acc, relatedMuscle] : acc + }, + [] as Muscle[] + ) ) } diff --git a/src/store/routerStore.ts b/src/store/routerStore.ts index 0fa1ff6..997a281 100644 --- a/src/store/routerStore.ts +++ b/src/store/routerStore.ts @@ -77,7 +77,10 @@ export const syncHistoryWithStore = (history: History.History, store: RouterStor store._updateLocation(location) } - const unsubscribeFromHistory = history.listen(handleLocationChange) + const unsubscribeFromHistory = history.listen(updatedLocation => { + console.log('updated Location from unsubscribeFromHistory', updatedLocation) + handleLocationChange(updatedLocation as Location) + }) handleLocationChange(history.location as any) const subscribe = (listener: History.LocationListener) => { diff --git a/src/translation.ts b/src/translation.ts new file mode 100644 index 0000000..ca6af2d --- /dev/null +++ b/src/translation.ts @@ -0,0 +1,46 @@ +// import * as Mobx from 'mobx' +import * as ReactIntl from 'react-intl' +import * as _ from 'lodash' + +export type Strings = { [key: string]: string } + +const TranslationsBridge: { + translations: { [key: string]: Strings } + messages: ReactIntl.Messages + hasTranslationsLoaded: boolean + defineMessages: (messages: ReactIntl.Messages) => ReactIntl.Messages + loadTranslations: (translations: { [key: string]: Strings }) => void + locales: string[] +} = { + translations: {}, + messages: {}, + hasTranslationsLoaded: false, + loadTranslations: translations => { + _.assign(TranslationsBridge.translations, translations) + TranslationsBridge.hasTranslationsLoaded = true + checkTranslationsAvailable(TranslationsBridge.translations, TranslationsBridge.messages) + }, + defineMessages: messages => { + _.assign(TranslationsBridge.messages, messages) + if (TranslationsBridge.hasTranslationsLoaded) checkTranslationsAvailable(TranslationsBridge.translations, messages) + else console.log(`Translations hasn't loaded yet`) + return ReactIntl.defineMessages(messages) + }, + get locales() { + return Object.keys(this.translations).map(key => key) + } +} + +const checkTranslationsAvailable = async (translations: { [key: string]: Strings }, messages: ReactIntl.Messages) => { + Object.keys(translations).map(locale => { + const localeStrings = translations[locale] + Object.keys(messages).map(messageKey => { + const message = messages[messageKey] + if (!localeStrings[message.id]) { + console.log(`Didn't find ${locale.toUpperCase()} translation for ${messageKey}(${message.id})`) + } + }) + }) +} + +export default TranslationsBridge diff --git a/translationRunner.js b/translationRunner.js new file mode 100644 index 0000000..9b83e91 --- /dev/null +++ b/translationRunner.js @@ -0,0 +1,19 @@ +import manageTranslations, { readMessageFiles, createSingleMessagesFile } from 'react-intl-translations-manager' + +const messagesDir = 'extracted-messages' +const translationsDir = 'src/app/l10n/' +const singleMessageFileDir = '.' + +manageTranslations({ + messagesDirectory: messagesDir, + translationsDirectory: translationsDir, + languages: ['en', 'ru'] +}) + +const extractedMessages = readMessageFiles(messagesDir) + +createSingleMessagesFile({ + messages: extractedMessages, + directory: singleMessageFileDir, + jsonSpaceIndentation: 4 +}) diff --git a/tsconfig.json b/tsconfig.json index f941dcc..2f0b8d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "allowJs": true, "suppressImplicitAnyIndexErrors": true, "experimentalDecorators": true, - "sourceMap": false + "sourceMap": false, + "strictFunctionTypes": false }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["android", "build", "ios", "node_modules"]