├── CONTRIBUTING.md ├── CNAME ├── app ├── .watchmanconfig ├── .node-version ├── .ruby-version ├── app.json ├── .bundle │ └── config ├── tsconfig.json ├── assets │ ├── icon.png │ └── splash.png ├── ios │ ├── app │ │ ├── splash.png │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── 29.png │ │ │ │ ├── 40.png │ │ │ │ ├── 57.png │ │ │ │ ├── 58.png │ │ │ │ ├── 60.png │ │ │ │ ├── 80.png │ │ │ │ ├── 87.png │ │ │ │ ├── 1024.png │ │ │ │ ├── 114.png │ │ │ │ ├── 120.png │ │ │ │ ├── 180.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.h │ │ ├── main.m │ │ ├── AppDelegate.mm │ │ ├── LaunchScreen.storyboard │ │ └── Info.plist │ ├── app.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── app.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── app.xcscheme │ ├── .xcode.env │ ├── appTests │ │ ├── Info.plist │ │ └── appTests.m │ ├── Podfile │ └── Podfile.lock ├── android │ ├── app │ │ ├── debug.keystore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── drawable-hdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── values │ │ │ │ │ │ ├── styles.xml │ │ │ │ │ │ └── strings.xml │ │ │ │ │ └── layout │ │ │ │ │ │ └── launch_screen.xml │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── koreanthinker │ │ │ │ │ └── translators │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ ├── debug │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── app │ │ │ │ │ └── ReactNativeFlipper.java │ │ │ └── release │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── koreanthinker │ │ │ │ └── translators │ │ │ │ └── ReactNativeFlipper.java │ │ ├── proguard-rules.pro │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ ├── gradlew.bat │ └── gradlew ├── babel.config.js ├── .prettierrc.js ├── src │ ├── constants │ │ ├── declarations.d.ts │ │ ├── types.ts │ │ ├── styles.ts │ │ └── values.ts │ ├── hooks │ │ ├── useRoute.ts │ │ ├── useNavigation.ts │ │ └── useKeyboard.ts │ ├── components │ │ ├── BaseButton.tsx │ │ ├── Typography.tsx │ │ ├── BaseInput.tsx │ │ └── BorderlessButton.tsx │ ├── navigations │ │ ├── index.tsx │ │ ├── HomeDrawerNavigation.tsx │ │ └── RootStackNavigation.tsx │ ├── screens │ │ ├── OssScreen │ │ │ ├── index.tsx │ │ │ ├── OssScreenHeader.tsx │ │ │ └── OssScreenOssCard.tsx │ │ ├── HistoryScreen │ │ │ ├── index.tsx │ │ │ ├── HistoryScreenHeader.tsx │ │ │ └── HistoryScreenHistoryCard.tsx │ │ ├── CardSequenceScreen │ │ │ ├── CardSequenceScreenHeader.tsx │ │ │ ├── index.tsx │ │ │ └── CardSequenceScreenTranslatorCard.tsx │ │ ├── FullScreen │ │ │ └── index.tsx │ │ ├── HomeScreen │ │ │ ├── HomeScreenHeader.tsx │ │ │ ├── HomeScreenInput.tsx │ │ │ ├── index.tsx │ │ │ ├── HomeScreenRecentCard.tsx │ │ │ ├── HomeScreenLanguageSelector.tsx │ │ │ └── HomeScreenTranslatedCard.tsx │ │ ├── HomeDrawerScreen │ │ │ └── index.tsx │ │ └── CreditScreen │ │ │ └── index.tsx │ ├── util │ │ └── languageTo.ts │ └── context │ │ ├── CardSequenceContext.tsx │ │ ├── HistoryContext.tsx │ │ └── TranslateContext.tsx ├── Gemfile ├── index.js ├── __tests__ │ └── App-test.tsx ├── metro.config.js ├── .eslintrc.js ├── .gitignore ├── App.tsx ├── Gemfile.lock └── package.json ├── web ├── jest.config.js ├── src │ ├── react-app-env.d.ts │ ├── constants │ │ ├── types.ts │ │ ├── styles.ts │ │ └── values.ts │ ├── setupTests.ts │ ├── pages │ │ ├── SupportPage │ │ │ ├── index.tsx │ │ │ └── SupportPage.spec.tsx │ │ ├── PrivacyPolicyPage │ │ │ ├── PrivacyPolicyPage.spec.tsx │ │ │ └── index.tsx │ │ └── HomePage │ │ │ ├── HomePageInput.tsx │ │ │ ├── HomePage.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── HomePageHeader.tsx │ │ │ ├── HomePageTranslatedCard.tsx │ │ │ └── HomePageLanguageSelector.tsx │ ├── index.css │ ├── index.tsx │ ├── reportWebVitals.ts │ ├── App.tsx │ ├── util │ │ ├── languageTo.spec.ts │ │ └── languageTo.ts │ └── context │ │ ├── TranslateContext.tsx │ │ └── TranslateContext.spec.tsx ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ ├── 404.html │ └── index.html ├── .prettierrc.js ├── .gitignore ├── .eslintrc.js ├── tsconfig.json └── package.json ├── .gitignore ├── .DS_Store ├── .github ├── .DS_Store └── workflows │ ├── ci-app.yml │ ├── ci-web.yml │ └── cd-web.yml ├── translators.code-workspace ├── .vscode └── extensions.json └── README.md /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | translators.kr -------------------------------------------------------------------------------- /app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /app/.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /app/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/android/app/translators_keystore.jks 2 | -------------------------------------------------------------------------------- /app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "displayName": "app" 4 | } -------------------------------------------------------------------------------- /web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/.DS_Store -------------------------------------------------------------------------------- /app/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.github/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/.github/.DS_Store -------------------------------------------------------------------------------- /app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/assets/icon.png -------------------------------------------------------------------------------- /app/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/assets/splash.png -------------------------------------------------------------------------------- /app/ios/app/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/splash.png -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/web/public/logo192.png -------------------------------------------------------------------------------- /web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/web/public/logo512.png -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/debug.keystore -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/ios/app/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /web/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: true, 4 | trailingComma: 'all', 5 | arrowParens: 'avoid' 6 | }; -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: ['react-native-reanimated/plugin'], 4 | }; 5 | -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/ios/app/Images.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /translators.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "app" 5 | }, 6 | { 7 | "path": "web" 8 | } 9 | ], 10 | "settings": {} 11 | } -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krtk-dev/translators/HEAD/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: false, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /web/src/constants/types.ts: -------------------------------------------------------------------------------- 1 | import { LANGUAGES } from './values'; 2 | 3 | export type Translator = 'google' | 'papago' | 'kakao'; 4 | export type Language = typeof LANGUAGES[number]; 5 | -------------------------------------------------------------------------------- /app/src/constants/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React from 'react'; 3 | import {SvgProps} from 'react-native-svg'; 4 | const content: React.FC; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 3 | ruby File.read(File.join(__dir__, '.ruby-version')).strip 4 | 5 | gem 'cocoapods', '~> 1.11', '>= 1.11.3' 6 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import {name as appName} from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dsznajder.es7-react-js-snippets", 4 | "dbaeumer.vscode-eslint", 5 | "Orta.vscode-jest", 6 | "esbenp.prettier-vscode", 7 | "styled-components.vscode-styled-components" 8 | ] 9 | } -------------------------------------------------------------------------------- /web/src/constants/styles.ts: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | white: '#FFF', 3 | black: '#000', 4 | red: '#E44034', 5 | papago: '#34A855', 6 | kakao: '#FABC05', 7 | google: '#1A73E8', 8 | }; 9 | 10 | export const BREAK_POINT = '768px'; 11 | -------------------------------------------------------------------------------- /app/ios/app/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /web/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'app' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/react-native-gradle-plugin') 5 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/app.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/ios/app.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/app.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/hooks/useRoute.ts: -------------------------------------------------------------------------------- 1 | import {RouteProp, useRoute as _useRoute} from '@react-navigation/core'; 2 | 3 | import {NavigationParamList} from '../navigations'; 4 | 5 | const useRoute = () => 6 | _useRoute>(); 7 | 8 | export default useRoute; 9 | -------------------------------------------------------------------------------- /app/ios/app.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/__tests__/App-test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | // renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /app/src/components/BaseButton.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, PressableProps, StyleSheet} from 'react-native'; 2 | import React from 'react'; 3 | 4 | const BaseButton: React.FC = props => ( 5 | 6 | ); 7 | 8 | export default BaseButton; 9 | 10 | const styles = StyleSheet.create({}); 11 | -------------------------------------------------------------------------------- /app/src/hooks/useNavigation.ts: -------------------------------------------------------------------------------- 1 | import {StackNavigationProp} from '@react-navigation/stack'; 2 | import {NavigationParamList} from '../navigations'; 3 | import {useNavigation as _useNavigation} from '@react-navigation/core'; 4 | 5 | const useNavigation = () => 6 | _useNavigation>(); 7 | 8 | export default useNavigation; 9 | -------------------------------------------------------------------------------- /app/src/constants/types.ts: -------------------------------------------------------------------------------- 1 | import {LanguageCode} from 'react-native-translator'; 2 | import {LANGUAGES_CODES} from './values'; 3 | 4 | export interface History { 5 | id: string; 6 | fromLanguage: LanguageCode<'google'>; 7 | toLanguage: LanguageCode<'google'>; 8 | text: string; 9 | } 10 | 11 | export type TranslatorType = 'google' | 'papago' | 'kakao'; 12 | -------------------------------------------------------------------------------- /app/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /app/src/components/Typography.tsx: -------------------------------------------------------------------------------- 1 | import {Text, TextProps} from 'react-native'; 2 | 3 | import React from 'react'; 4 | import {COLORS} from '../constants/styles'; 5 | 6 | const Typography: React.FC = ({...props}) => ( 7 | 12 | ); 13 | 14 | export default Typography; 15 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web/src/pages/SupportPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | const SupportPage = () => { 4 | useEffect(() => { 5 | window.location.href = 'mailto:coderhyun476@gmail.com'; 6 | }, []); 7 | 8 | return ( 9 |
10 |

고객지원

11 | coderhyun476@gmail.com 12 |
13 | ); 14 | }; 15 | 16 | export default SupportPage; 17 | -------------------------------------------------------------------------------- /web/src/pages/PrivacyPolicyPage/PrivacyPolicyPage.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import PrivacyPolicyPage from '.'; 3 | 4 | describe('', () => { 5 | it('한글을 지원합니다.', async () => { 6 | const { container } = render(); 7 | expect(container.innerHTML).toContain('개인정보처리방침'); 8 | // expect(container.innerHTML).toContain('PrivacyPolicy'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /web/src/pages/SupportPage/SupportPage.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import SupportPage from '.'; 3 | 4 | describe('', () => { 5 | it('이메일로 연결합니다.', async () => { 6 | const { container } = render(); 7 | const aTag = container.querySelector('a'); 8 | expect(aTag?.innerHTML).toBe('coderhyun476@gmail.com'); 9 | expect(aTag?.href).toBe('mailto:coderhyun476@gmail.com'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | width: 100vw; 4 | height: 100vh; 5 | } 6 | 7 | div { 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 19 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 20 | sans-serif; 21 | -webkit-font-smoothing: antialiased; 22 | } 23 | -------------------------------------------------------------------------------- /app/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById('root'), 15 | ); 16 | 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /web/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /app/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | # NODE_BINARY variable contains the PATH to the node executable. 6 | # 7 | # Customize the NODE_BINARY variable here. 8 | # For example, to use nvm with brew, add the following line 9 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 10 | export NODE_BINARY=$(command -v node) 11 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3가지 번역기 3 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlR+X7ZIkVhBBAsCtQYWVsbfKZdxhAnHLTXdK+dmdF/2rP0WzzlG/7IhGhbHijIhQ0t7JjJheqYdNTzfsUYzgDqNoeVdOSfLFUu3DDXKl5YtBPxC4YqU2PH4x+ftM926If/4deU+gu9fDP3pL4ALj0Xr5oUpvmQiT0jW/LATE9TgQXhaD+E+TeCsXIjVrtTAOTRJjn1T8AL8RrDCDeNVz/EXxAgu8nM95AFhJUNnhmGIZ9tr1q2536hBggYEIDTAXGUGIADOHWoDJmj1+6Q0Loaje+X30dnZbS2oLOJngqaEPjbSxhLAh3LWdrvuV0NuAscY787oVuehU5wPz3+cdRQIDAQAB 4 | -------------------------------------------------------------------------------- /app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /web/src/constants/values.ts: -------------------------------------------------------------------------------- 1 | import { Translator } from './types'; 2 | 3 | export const TRANSLATORS: Translator[] = ['google', 'papago', 'kakao']; 4 | export const LANGUAGES = [ 5 | 'kr', 6 | 'en', 7 | 'jp', 8 | 'cn', 9 | 'vi', 10 | 'de', 11 | 'es', 12 | 'fr', 13 | 'it', 14 | ] as const; 15 | 16 | export const PLAYSTORE_URL = 17 | 'https://play.google.com/store/apps/details?id=com.koreanthinker.translators'; 18 | export const APPSTORE_URL = 'https://apps.apple.com/app/id1611097883'; 19 | export const GITHUB_URL = 'https://github.com/krtk-dev/translators'; 20 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/layout/launch_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "3가지 번역기", 3 | "name": "3가지 번역기 비교하다", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } -------------------------------------------------------------------------------- /app/src/components/BaseInput.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, Text, TextInput, TextInputProps} from 'react-native'; 2 | import React from 'react'; 3 | import {COLORS} from '../constants/styles'; 4 | 5 | const BaseInput = React.forwardRef((props, ref) => ( 6 | 12 | )); 13 | 14 | export default BaseInput; 15 | 16 | const styles = StyleSheet.create({ 17 | input: { 18 | padding: 0, 19 | margin: 0, 20 | color: COLORS.black, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:import/errors', 10 | 'plugin:import/warnings', 11 | 'plugin:import/typescript', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'plugin:prettier/recommended', 14 | ], 15 | plugins: ['@typescript-eslint', 'import', 'prettier', 'react', 'react-hooks'], 16 | rules: { 17 | 'import/no-named-as-default-member': 'off', 18 | '@typescript-eslint/no-explicit-any': 'off', 19 | '@typescript-eslint/ban-ts-comment': 'off', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | ignorePatterns: ['/coverage'], 7 | overrides: [ 8 | { 9 | files: ['*.ts', '*.tsx'], 10 | rules: { 11 | '@typescript-eslint/no-shadow': ['error'], 12 | 'no-shadow': 'off', 13 | 'no-undef': 'off', 14 | '@typescript-eslint/no-unused-vars': 'off', 15 | 'react-native/no-inline-styles': 'off', 16 | curly: 'off', 17 | 'comma-dangle': 'off', 18 | 'react-hooks/exhaustive-deps': 'off', 19 | }, 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /app/src/components/BorderlessButton.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, PressableProps, StyleSheet} from 'react-native'; 2 | import React, {useState} from 'react'; 3 | 4 | const BorderlessButton: React.FC = props => { 5 | const [radius, setRadius] = useState(); 6 | 7 | return ( 8 | 10 | setRadius( 11 | Math.max(e.nativeEvent.layout.height, e.nativeEvent.layout.width) / 2, 12 | ) 13 | } 14 | android_ripple={{borderless: true, color: '#ccc', radius}} 15 | {...props} 16 | /> 17 | ); 18 | }; 19 | 20 | export default BorderlessButton; 21 | 22 | const styles = StyleSheet.create({}); 23 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "jsxImportSource": "@emotion/react" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import TranslateProvider from './context/TranslateContext'; 4 | import HomePage from './pages/HomePage'; 5 | import PrivacyPolicyPage from './pages/PrivacyPolicyPage'; 6 | import SupportPage from './pages/SupportPage'; 7 | 8 | const App = () => { 9 | return ( 10 | 11 | 12 | } /> 13 | } /> 14 | } /> 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /app/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "33.0.0" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. 10 | ndkVersion = "23.1.7779620" 11 | } 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | dependencies { 17 | classpath("com.android.tools.build:gradle:7.3.1") 18 | classpath("com.facebook.react:react-native-gradle-plugin") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/constants/styles.ts: -------------------------------------------------------------------------------- 1 | import {Dimensions} from 'react-native'; 2 | import {getStatusBarHeight} from 'react-native-status-bar-height'; 3 | 4 | export const WIDTH = Dimensions.get('window').width; 5 | export const HEIGHT = Dimensions.get('screen').height; 6 | export const STATUSBAR_HEIGHT = getStatusBarHeight(); 7 | 8 | export const COLORS = { 9 | white: '#FFF', 10 | black: '#000', 11 | red: '#E44034', 12 | papago: '#34A855', 13 | kakao: '#FABC05', 14 | google: '#1A73E8', 15 | }; 16 | 17 | export const SHADOW = { 18 | shadowColor: '#000', 19 | shadowOffset: { 20 | width: 0, 21 | height: 2, 22 | }, 23 | shadowOpacity: 0.23, 24 | shadowRadius: 2.62, 25 | 26 | elevation: 4, 27 | }; 28 | -------------------------------------------------------------------------------- /app/src/navigations/index.tsx: -------------------------------------------------------------------------------- 1 | import {DefaultTheme, NavigationContainer} from '@react-navigation/native'; 2 | import RootStackNavigation, {RootStackParamList} from './RootStackNavigation'; 3 | 4 | import React from 'react'; 5 | import {COLORS} from '../constants/styles'; 6 | import {DrawerParamList} from './HomeDrawerNavigation'; 7 | 8 | export type NavigationParamList = RootStackParamList & DrawerParamList; 9 | 10 | const Navigation = () => { 11 | return ( 12 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Navigation; 23 | -------------------------------------------------------------------------------- /app/android/app/src/release/java/com/koreanthinker/translators/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.koreanthinker.translators; 8 | import android.content.Context; 9 | import com.facebook.react.ReactInstanceManager; 10 | /** 11 | * Class responsible of loading Flipper inside your React Native application. This is the release 12 | * flavor of it so it's empty as we don't want to load Flipper. 13 | */ 14 | public class ReactNativeFlipper { 15 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 16 | // Do nothing as we don't want to initialize Flipper on Release. 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/navigations/HomeDrawerNavigation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createDrawerNavigator} from '@react-navigation/drawer'; 3 | import HomeDrawerScreen from '../screens/HomeDrawerScreen'; 4 | import HomeScreen from '../screens/HomeScreen'; 5 | 6 | export type DrawerParamList = { 7 | Home: undefined; 8 | }; 9 | const Drawer = createDrawerNavigator(); 10 | 11 | const HomeDrawerNavigation = () => { 12 | return ( 13 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default HomeDrawerNavigation; 27 | -------------------------------------------------------------------------------- /app/src/constants/values.ts: -------------------------------------------------------------------------------- 1 | import {Platform} from 'react-native'; 2 | import {LanguageCode} from 'react-native-translator'; 3 | 4 | export const IS_ANDROID = Platform.OS === 'android'; 5 | export const IS_IOS = Platform.OS === 'ios'; 6 | 7 | export const STOREAGE_HISTORYS_ID = '@HISTORYS'; 8 | export const STOREAGE_CARD_SEQUENCE_ID = '@CARD_SEQUENCE'; 9 | 10 | // From google language code 11 | // https://github.com/KoreanThinker/react-native-translator#support-languages 12 | export const LANGUAGES_CODES: LanguageCode<'google'>[] = [ 13 | 'ko', 14 | 'en', 15 | 'ja', 16 | 'zh-CN', 17 | 'zh-TW', 18 | 'vi', 19 | 'de', 20 | 'es', 21 | 'fr', 22 | 'it', 23 | 'ru', 24 | 'th', 25 | 'id', 26 | ]; 27 | export const PLAYSTORE_URL = 28 | 'https://play.google.com/store/apps/details?id=com.koreanthinker.translators'; 29 | export const RATE_UNIT = 30; 30 | -------------------------------------------------------------------------------- /app/ios/appTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 2.1.5 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 27 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/screens/OssScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, StyleSheet, FlatList} from 'react-native'; 3 | import {COLORS, STATUSBAR_HEIGHT} from '../../constants/styles'; 4 | import oss from '../../assets/oss.json'; 5 | import OssScreenHeader from './OssScreenHeader'; 6 | import OssScreenOssCard from './OssScreenOssCard'; 7 | 8 | const DATA = Object.keys(oss).map(key => ({ 9 | name: key, 10 | //@ts-ignore 11 | ...oss[key], 12 | })); 13 | 14 | const OssScreen = () => { 15 | return ( 16 | 17 | 18 | item.name} 21 | renderItem={({item}) => } 22 | /> 23 | 24 | ); 25 | }; 26 | 27 | const styles = StyleSheet.create({ 28 | container: { 29 | flex: 1, 30 | backgroundColor: COLORS.red, 31 | paddingTop: STATUSBAR_HEIGHT, 32 | }, 33 | }); 34 | 35 | export default OssScreen; 36 | -------------------------------------------------------------------------------- /.github/workflows/ci-app.yml: -------------------------------------------------------------------------------- 1 | name: CI App 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "app/**" 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | paths: 12 | - "app/**" 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | platform: [ubuntu-latest] 19 | node: ["16"] 20 | name: Node ${{ matrix.node }} (${{ matrix.platform }}) 21 | runs-on: ${{ matrix.platform }} 22 | defaults: 23 | run: 24 | working-directory: app 25 | 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.node }} 32 | 33 | - name: Install modules 34 | run: yarn 35 | 36 | - name: Run lint 37 | run: yarn lint 38 | 39 | - name: Run test 40 | run: yarn test:coverage 41 | 42 | - name: Upload coverage to Codecov 43 | uses: codecov/codecov-action@v2 -------------------------------------------------------------------------------- /.github/workflows/ci-web.yml: -------------------------------------------------------------------------------- 1 | name: CI Web 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "web/**" 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | paths: 12 | - "web/**" 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | platform: [ubuntu-latest] 19 | node: ["16"] 20 | name: Node ${{ matrix.node }} (${{ matrix.platform }}) 21 | runs-on: ${{ matrix.platform }} 22 | defaults: 23 | run: 24 | working-directory: web 25 | 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.node }} 32 | 33 | - name: Install modules 34 | run: yarn 35 | 36 | - name: Run lint 37 | run: yarn lint 38 | 39 | - name: Run test 40 | run: yarn test:coverage 41 | 42 | - name: Upload coverage to Codecov 43 | uses: codecov/codecov-action@v2 -------------------------------------------------------------------------------- /.github/workflows/cd-web.yml: -------------------------------------------------------------------------------- 1 | name: CD Web 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "web/**" 9 | 10 | jobs: 11 | build-and-deploy: 12 | strategy: 13 | matrix: 14 | platform: [ubuntu-latest] 15 | node: ["16"] 16 | name: Node ${{ matrix.node }} (${{ matrix.platform }}) 17 | runs-on: ${{ matrix.platform }} 18 | defaults: 19 | run: 20 | working-directory: web 21 | 22 | steps: 23 | - uses: actions/checkout@main 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node }} 27 | 28 | - name: Install packages and build 29 | run: | 30 | yarn 31 | yarn build 32 | 33 | - name: Copy CNAME 34 | run: cp ../CNAME ./build/CNAME 35 | 36 | - name: Deploy 37 | uses: peaceiris/actions-gh-pages@v3 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: ./web/build -------------------------------------------------------------------------------- /app/src/screens/HistoryScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import {FlatList, StyleSheet, Text, View} from 'react-native'; 2 | import React, {useContext} from 'react'; 3 | import HistoryScreenHeader from './HistoryScreenHeader'; 4 | import {HistoryContext} from '../../context/HistoryContext'; 5 | import {TranslateContext} from '../../context/TranslateContext'; 6 | import HistoryScreenHistoryCard from './HistoryScreenHistoryCard'; 7 | 8 | const HistoryScreen = () => { 9 | const {historys} = useContext(HistoryContext); 10 | 11 | return ( 12 | 13 | 14 | } 18 | ListFooterComponent={} 19 | renderItem={({item}) => } 20 | /> 21 | 22 | ); 23 | }; 24 | 25 | export default HistoryScreen; 26 | 27 | const styles = StyleSheet.create({}); 28 | -------------------------------------------------------------------------------- /web/src/pages/HomePage/HomePageInput.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import React, { useContext } from 'react'; 3 | import { BREAK_POINT } from '../../constants/styles'; 4 | import { TranslateContext } from '../../context/TranslateContext'; 5 | 6 | const Container = styled.div` 7 | flex: 1; 8 | align-items: center; 9 | padding: 32px 72px; 10 | @media (max-width: ${BREAK_POINT}) { 11 | flex: none; 12 | padding: 64px 16px; 13 | } 14 | `; 15 | 16 | const Input = styled.textarea` 17 | width: 100%; 18 | min-height: 50vh; 19 | border: none; 20 | resize: none; 21 | outline-color: transparent; 22 | @media (max-width: ${BREAK_POINT}) { 23 | flex: none; 24 | min-height: auto; 25 | } 26 | `; 27 | 28 | const HomePageInput = () => { 29 | const { text, onChangeText } = useContext(TranslateContext); 30 | 31 | return ( 32 | 33 | onChangeText(e.target.value)} 37 | /> 38 | 39 | ); 40 | }; 41 | 42 | export default HomePageInput; 43 | -------------------------------------------------------------------------------- /web/src/pages/PrivacyPolicyPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PrivacyPolicyPage = () => { 4 | return ( 5 |

6 |

개인정보처리방침

7 |

8 | "KoreanThinker"에서는 이용자분의 개인정보 보호를 중요시하여 [정보통신망 9 | 이용 촉진 및 정보보호]에 관한 법률을 준수하고 있습니다. 10 |

11 |

1. 개인정보를 수집하는 경우

12 |

13 | 번역을 할때마다 앱 내부에 번역기록을 저장하지만 본 회사는 어떠한 유저의 14 | 정보도 수집하지 않습니다. 15 |

16 |

2. 개인정보 항목 및 수집목적

17 |

번역기록, 카드 순서기록 목적

18 |

3. 수집된 개인정보 이용 및 보유기간

19 |

20 | 본 정보는 회사가 아닌 유저 앱 내부 저장소에 보간되며 앱을 삭제하기 21 | 전까지 보간됩니다. 22 |

23 |

4. 수집된 정보 파기 방법

24 |

앱 삭제

25 |

5. 개발자 연락처

26 |

coderhyun476@gmail.com

27 |

28 |

아래의 링크 항목을 준수하고 있습니다.

29 | 30 | https://play.google.com/about/privacy-security-deception/user-data/ 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default PrivacyPolicyPage; 37 | -------------------------------------------------------------------------------- /app/src/screens/OssScreen/OssScreenHeader.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, Text, View} from 'react-native'; 2 | import React from 'react'; 3 | import BaseButton from '../../components/BaseButton'; 4 | import useNavigation from '../../hooks/useNavigation'; 5 | import Icon from 'react-native-vector-icons/MaterialIcons'; 6 | import {COLORS} from '../../constants/styles'; 7 | 8 | const OssScreenHeader = () => { 9 | const {goBack} = useNavigation(); 10 | 11 | return ( 12 | goBack()} style={styles.container}> 13 | 14 | 15 | 16 | 뒤로 17 | 18 | ); 19 | }; 20 | 21 | export default OssScreenHeader; 22 | 23 | const styles = StyleSheet.create({ 24 | container: { 25 | width: '100%', 26 | height: 56, 27 | flexDirection: 'row', 28 | alignItems: 'center', 29 | }, 30 | iconContainer: { 31 | width: 56, 32 | alignItems: 'center', 33 | }, 34 | text: { 35 | fontSize: 16, 36 | color: COLORS.white, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /web/src/util/languageTo.spec.ts: -------------------------------------------------------------------------------- 1 | import { LANGUAGES } from '../constants/values'; 2 | import languageTo from './languageTo'; 3 | 4 | describe('util/languageTo', () => { 5 | it('korean', () => { 6 | const koreans = LANGUAGES.map(language => languageTo.korean(language)); 7 | expect(koreans.every(v => !!v && v !== '오류')).toBeTruthy(); 8 | }); 9 | it('ttsLanguage', () => { 10 | const ttsLanguage = LANGUAGES.map(language => 11 | languageTo.ttsLanguage(language), 12 | ); 13 | expect(ttsLanguage.every(v => !!v)).toBeTruthy(); 14 | }); 15 | it('kakaoLanguage', () => { 16 | const kakaoLanguage = LANGUAGES.map(language => 17 | languageTo.kakaoLanguage(language), 18 | ); 19 | expect(kakaoLanguage.every(v => !!v)).toBeTruthy(); 20 | }); 21 | it('papagoLanguage', () => { 22 | const papagoLanguage = LANGUAGES.map(language => 23 | languageTo.papagoLanguage(language), 24 | ); 25 | expect(papagoLanguage.every(v => !!v)).toBeTruthy(); 26 | }); 27 | it('googleLanguage', () => { 28 | const googleLanguage = LANGUAGES.map(language => 29 | languageTo.googleLanguage(language), 30 | ); 31 | expect(googleLanguage.every(v => !!v)).toBeTruthy(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | ios/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | 37 | # node.js 38 | # 39 | node_modules/ 40 | npm-debug.log 41 | yarn-error.log 42 | 43 | # fastlane 44 | # 45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 46 | # screenshots whenever they are needed. 47 | # For more information about the recommended setup visit: 48 | # https://docs.fastlane.tools/best-practices/source-control/ 49 | 50 | **/fastlane/report.xml 51 | **/fastlane/Preview.html 52 | **/fastlane/screenshots 53 | **/fastlane/test_output 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # Ruby / CocoaPods 59 | /ios/Pods/ 60 | /vendor/bundle/ 61 | 62 | # Temporary files created by Metro to check the health of the file watcher 63 | .metro-health-check* 64 | -------------------------------------------------------------------------------- /web/src/pages/HomePage/HomePage.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import HomePage from '.'; 3 | import { 4 | APPSTORE_URL, 5 | GITHUB_URL, 6 | PLAYSTORE_URL, 7 | } from '../../constants/values'; 8 | import TranslateProvider from '../../context/TranslateContext'; 9 | 10 | describe('', () => { 11 | it('플레이스토어와 앱스토어 링크를 제공합니다.', async () => { 12 | const { container } = render(, { 13 | wrapper: ({ children }) => ( 14 | {children} 15 | ), 16 | }); 17 | expect(container.innerHTML).toContain(APPSTORE_URL); 18 | expect(container.innerHTML).toContain(PLAYSTORE_URL); 19 | }); 20 | it('깃헙으로 통하는 링크를 제공합니다.', async () => { 21 | const { container } = render(, { 22 | wrapper: ({ children }) => ( 23 | {children} 24 | ), 25 | }); 26 | expect(container.innerHTML).toContain(GITHUB_URL); 27 | }); 28 | it('번역결과를 보여줄 3가지 카드를 제공합니다.', async () => { 29 | const { container } = render(, { 30 | wrapper: ({ children }) => ( 31 | {children} 32 | ), 33 | }); 34 | expect(container.querySelectorAll('.translated-card')).toHaveLength(3); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /app/src/screens/CardSequenceScreen/CardSequenceScreenHeader.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, Text, View} from 'react-native'; 2 | import React from 'react'; 3 | import {COLORS} from '../../constants/styles'; 4 | import Typography from '../../components/Typography'; 5 | import Icon from 'react-native-vector-icons/MaterialIcons'; 6 | import BaseButton from '../../components/BaseButton'; 7 | import useNavigation from '../../hooks/useNavigation'; 8 | 9 | const CardSequenceScreenHeader = () => { 10 | const {goBack} = useNavigation(); 11 | 12 | return ( 13 | 14 | 뒤로 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default CardSequenceScreenHeader; 23 | 24 | const styles = StyleSheet.create({ 25 | container: { 26 | width: '100%', 27 | height: 56, 28 | flexDirection: 'row', 29 | justifyContent: 'space-between', 30 | alignItems: 'center', 31 | paddingLeft: 16, 32 | }, 33 | text: { 34 | color: COLORS.white, 35 | fontSize: 16, 36 | }, 37 | iconContainer: { 38 | width: 56, 39 | height: 56, 40 | alignItems: 'center', 41 | justifyContent: 'center', 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /app/ios/app/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "RNSplashScreen.h" 3 | 4 | #import 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 9 | { 10 | self.moduleName = @"app"; 11 | // You can add your custom initial props in the dictionary below. 12 | // They will be passed down to the ViewController used by React Native. 13 | self.initialProps = @{}; 14 | 15 | [super application:application didFinishLaunchingWithOptions:launchOptions]; 16 | [RNSplashScreen show]; 17 | 18 | return YES; 19 | } 20 | 21 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 22 | { 23 | #if DEBUG 24 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 25 | #else 26 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 27 | #endif 28 | } 29 | 30 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 31 | /// 32 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 33 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 34 | /// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. 35 | - (BOOL)concurrentRootEnabled 36 | { 37 | return true; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /app/src/screens/FullScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, StyleSheet, Text, View} from 'react-native'; 2 | import React from 'react'; 3 | import useRoute from '../../hooks/useRoute'; 4 | import {AutoSizeText, ResizeTextMode} from 'react-native-auto-size-text'; 5 | import {COLORS, HEIGHT, WIDTH} from '../../constants/styles'; 6 | import useNavigation from '../../hooks/useNavigation'; 7 | 8 | export interface FullScreenProps { 9 | color: string; 10 | content: string; 11 | } 12 | 13 | const FullScreen = () => { 14 | const { 15 | params: {color, content}, 16 | } = useRoute<'Full'>(); 17 | const {goBack} = useNavigation(); 18 | 19 | return ( 20 | 23 | 24 | 25 | {content} 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default FullScreen; 33 | 34 | const styles = StyleSheet.create({ 35 | container: { 36 | flex: 1, 37 | alignItems: 'center', 38 | justifyContent: 'center', 39 | }, 40 | textContainer: { 41 | width: HEIGHT - 160, 42 | height: WIDTH - 80, 43 | transform: [{rotate: '90deg'}], 44 | }, 45 | text: { 46 | color: COLORS.white, 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /app/src/screens/OssScreen/OssScreenOssCard.tsx: -------------------------------------------------------------------------------- 1 | import {Linking, StyleSheet} from 'react-native'; 2 | import React from 'react'; 3 | import BaseButton from '../../components/BaseButton'; 4 | import Typography from '../../components/Typography'; 5 | import {COLORS} from '../../constants/styles'; 6 | 7 | interface OssScreenOssCardProps { 8 | name: string; 9 | licenses: string; 10 | repository: string; 11 | } 12 | 13 | const OssScreenOssCard: React.FC = props => { 14 | const {licenses, name, repository} = props; 15 | 16 | return ( 17 | Linking.openURL(repository)} 19 | style={styles.container}> 20 | {name} 21 | {repository} 22 | {licenses} 23 | 24 | ); 25 | }; 26 | 27 | export default OssScreenOssCard; 28 | 29 | const styles = StyleSheet.create({ 30 | container: { 31 | width: '100%', 32 | paddingHorizontal: 16, 33 | paddingVertical: 8, 34 | }, 35 | name: { 36 | color: COLORS.white, 37 | fontSize: 16, 38 | }, 39 | repository: { 40 | color: COLORS.white, 41 | fontSize: 12, 42 | marginTop: 4, 43 | }, 44 | licenses: { 45 | color: COLORS.white, 46 | fontSize: 12, 47 | marginTop: 2, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /web/src/pages/HomePage/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import React from 'react'; 3 | import { BREAK_POINT } from '../../constants/styles'; 4 | import { TRANSLATORS } from '../../constants/values'; 5 | import HomePageHeader from './HomePageHeader'; 6 | import HomePageInput from './HomePageInput'; 7 | import HomePageLanguageSelector from './HomePageLanguageSelector'; 8 | import HomePageTranslatedCard from './HomePageTranslatedCard'; 9 | 10 | const Container = styled.div` 11 | flex: 1; 12 | `; 13 | 14 | const ContentContainer = styled.div` 15 | width: 100%; 16 | flex-direction: row; 17 | @media (max-width: ${BREAK_POINT}) { 18 | flex-direction: column; 19 | } 20 | `; 21 | 22 | const TranslatedCardContainer = styled.div` 23 | flex: 1; 24 | padding: 0 16px; 25 | padding-top: 32px; 26 | align-items: center; 27 | @media (max-width: ${BREAK_POINT}) { 28 | flex: none; 29 | padding-top: 0px; 30 | } 31 | `; 32 | 33 | const HomePage = () => { 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | {TRANSLATORS.map(translator => ( 42 | 43 | ))} 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default HomePage; 51 | -------------------------------------------------------------------------------- /app/src/screens/CardSequenceScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, Text, View} from 'react-native'; 2 | import React, {useCallback, useContext} from 'react'; 3 | import {COLORS, STATUSBAR_HEIGHT} from '../../constants/styles'; 4 | import CardSequenceScreenHeader from './CardSequenceScreenHeader'; 5 | import DraggableFlatList, { 6 | DragEndParams, 7 | } from 'react-native-draggable-flatlist'; 8 | import {CardSequenceContext} from '../../context/CardSequenceContext'; 9 | import CardSequenceTranslatorCard from './CardSequenceScreenTranslatorCard'; 10 | import {TranslatorType} from '../../constants/types'; 11 | 12 | const CardSequenceScreen = () => { 13 | const {cardSequence, updateCardSequence} = useContext(CardSequenceContext); 14 | 15 | const onDragEnd = useCallback( 16 | ({data}: DragEndParams) => { 17 | updateCardSequence(data); 18 | }, 19 | [updateCardSequence], 20 | ); 21 | 22 | return ( 23 | 24 | 25 | } 28 | keyExtractor={item => item} 29 | onDragEnd={onDragEnd} 30 | /> 31 | 32 | ); 33 | }; 34 | 35 | export default CardSequenceScreen; 36 | 37 | const styles = StyleSheet.create({ 38 | container: { 39 | flex: 1, 40 | backgroundColor: COLORS.red, 41 | paddingTop: STATUSBAR_HEIGHT, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /app/src/screens/HomeScreen/HomeScreenHeader.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, StyleSheet, Text, View} from 'react-native'; 2 | import React from 'react'; 3 | import useNavigation from '../../hooks/useNavigation'; 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | import {COLORS, STATUSBAR_HEIGHT} from '../../constants/styles'; 6 | import Typography from '../../components/Typography'; 7 | import BorderlessButton from '../../components/BorderlessButton'; 8 | 9 | const HomeScreenHeader = () => { 10 | const {openDrawer}: any = useNavigation(); 11 | 12 | return ( 13 | 14 | openDrawer()} style={styles.menuBtn}> 15 | 16 | 17 | 3가지 번역기 비교하다 18 | 19 | ); 20 | }; 21 | 22 | export default HomeScreenHeader; 23 | 24 | const styles = StyleSheet.create({ 25 | container: { 26 | flexDirection: 'row', 27 | alignItems: 'center', 28 | width: '100%', 29 | height: 56 + STATUSBAR_HEIGHT, 30 | paddingTop: STATUSBAR_HEIGHT, 31 | backgroundColor: COLORS.red, 32 | zIndex: 99, 33 | }, 34 | menuBtn: { 35 | width: 56, 36 | height: 56, 37 | alignItems: 'center', 38 | justifyContent: 'center', 39 | }, 40 | title: { 41 | fontSize: 18, 42 | color: COLORS.white, 43 | fontWeight: 'bold', 44 | marginLeft: 16, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /app/App.tsx: -------------------------------------------------------------------------------- 1 | import {StatusBar, StyleSheet} from 'react-native'; 2 | import React, {useEffect} from 'react'; 3 | import SplashScreen from 'react-native-splash-screen'; 4 | import Navigation from './src/navigations'; 5 | import {LogBox} from 'react-native'; 6 | import {SafeAreaProvider} from 'react-native-safe-area-context'; 7 | import HistoryProvider from './src/context/HistoryContext'; 8 | import CardSequenceProvider from './src/context/CardSequenceContext'; 9 | import TranslateProvider from './src/context/TranslateContext'; 10 | 11 | LogBox.ignoreLogs([ 12 | "[react-native-gesture-handler] Seems like you're using an old API with gesture components, check out new Gestures system!", 13 | 'Sending `tts', 14 | '`new NativeEventEmitter()` was called', 15 | ]); 16 | 17 | const App = () => { 18 | useEffect(() => { 19 | setTimeout(() => { 20 | SplashScreen.hide(); 21 | }, 500); 22 | }, []); 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default App; 44 | 45 | const styles = StyleSheet.create({}); 46 | -------------------------------------------------------------------------------- /app/src/screens/HistoryScreen/HistoryScreenHeader.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, StyleSheet, Text, View} from 'react-native'; 2 | import React from 'react'; 3 | import {COLORS, SHADOW, STATUSBAR_HEIGHT} from '../../constants/styles'; 4 | import Typography from '../../components/Typography'; 5 | import Icon from 'react-native-vector-icons/MaterialIcons'; 6 | import useNavigation from '../../hooks/useNavigation'; 7 | import BorderlessButton from '../../components/BorderlessButton'; 8 | 9 | const HistoryScreenHeader = () => { 10 | const {goBack} = useNavigation(); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 번역 기록 18 | 19 | ); 20 | }; 21 | 22 | export default HistoryScreenHeader; 23 | 24 | const styles = StyleSheet.create({ 25 | container: { 26 | width: '100%', 27 | height: STATUSBAR_HEIGHT + 56, 28 | paddingTop: STATUSBAR_HEIGHT, 29 | backgroundColor: COLORS.red, 30 | flexDirection: 'row', 31 | alignItems: 'center', 32 | ...SHADOW, 33 | zIndex: 99, 34 | }, 35 | icon: { 36 | width: 56, 37 | height: 56, 38 | alignItems: 'center', 39 | justifyContent: 'center', 40 | }, 41 | title: { 42 | marginLeft: 8, 43 | fontSize: 18, 44 | color: COLORS.white, 45 | fontWeight: 'bold', 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /app/src/hooks/useKeyboard.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import {Keyboard} from 'react-native'; 3 | 4 | const useKeyboard = () => { 5 | const [keyboardHeight, setKeyboardHeight] = useState(0); 6 | const [keyboardShown, setKeyboardShown] = useState(false); 7 | const [duration, setDuration] = useState(0); 8 | 9 | useEffect(() => { 10 | const keyboardWillShowSubscription = Keyboard.addListener( 11 | 'keyboardWillShow', 12 | e => { 13 | setKeyboardShown(true); 14 | setKeyboardHeight(e.endCoordinates.height); 15 | setDuration(e.duration); 16 | }, 17 | ); 18 | const keyboardDidShowSubscription = Keyboard.addListener( 19 | 'keyboardDidShow', 20 | e => { 21 | setKeyboardShown(true); 22 | setKeyboardHeight(e.endCoordinates.height); 23 | setDuration(e.duration); 24 | }, 25 | ); 26 | const keyboardWillHideSubscription = Keyboard.addListener( 27 | 'keyboardWillHide', 28 | () => { 29 | setKeyboardShown(false); 30 | }, 31 | ); 32 | 33 | const keyboardDidHideSubscription = Keyboard.addListener( 34 | 'keyboardDidHide', 35 | () => { 36 | setKeyboardShown(false); 37 | }, 38 | ); 39 | 40 | return () => { 41 | keyboardWillShowSubscription.remove(); 42 | keyboardWillHideSubscription.remove(); 43 | keyboardDidShowSubscription.remove(); 44 | keyboardDidHideSubscription.remove(); 45 | }; 46 | }, []); 47 | 48 | return {keyboardHeight, keyboardShown, duration}; 49 | }; 50 | 51 | export default useKeyboard; 52 | -------------------------------------------------------------------------------- /app/src/navigations/RootStackNavigation.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createStackNavigator, 3 | CardStyleInterpolators, 4 | } from '@react-navigation/stack'; 5 | import React from 'react'; 6 | import CardSequenceScreen from '../screens/CardSequenceScreen'; 7 | import CreditScreen from '../screens/CreditScreen'; 8 | import FullScreen, {FullScreenProps} from '../screens/FullScreen'; 9 | import HistoryScreen from '../screens/HistoryScreen'; 10 | import OssScreen from '../screens/OssScreen'; 11 | import HomeDrawerNavigation from './HomeDrawerNavigation'; 12 | 13 | export type RootStackParamList = { 14 | HomeDrawer: undefined; 15 | Full: FullScreenProps; 16 | CardSequence: undefined; 17 | Credit: undefined; 18 | History: undefined; 19 | Oss: undefined; 20 | }; 21 | 22 | const RootStack = createStackNavigator(); 23 | 24 | const RootStackNavigation = () => { 25 | return ( 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default RootStackNavigation; 43 | -------------------------------------------------------------------------------- /app/src/screens/CardSequenceScreen/CardSequenceScreenTranslatorCard.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, StyleSheet} from 'react-native'; 2 | import React from 'react'; 3 | import {COLORS} from '../../constants/styles'; 4 | import Typography from '../../components/Typography'; 5 | import Icon from 'react-native-vector-icons/MaterialIcons'; 6 | import BaseButton from '../../components/BaseButton'; 7 | import {TranslatorType} from '../../constants/types'; 8 | import {RenderItemParams} from 'react-native-draggable-flatlist'; 9 | 10 | const CardSequenceTranslatorCard: React.FC< 11 | RenderItemParams 12 | > = props => { 13 | const {item, drag, isActive} = props; 14 | 15 | return ( 16 | 21 | {item.toUpperCase()} 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default CardSequenceTranslatorCard; 30 | 31 | const styles = StyleSheet.create({ 32 | container: { 33 | width: '100%', 34 | height: 56, 35 | flexDirection: 'row', 36 | justifyContent: 'space-between', 37 | alignItems: 'center', 38 | paddingLeft: 16, 39 | }, 40 | text: { 41 | color: COLORS.white, 42 | fontSize: 16, 43 | }, 44 | iconContainer: { 45 | width: 56, 46 | height: 56, 47 | alignItems: 'center', 48 | justifyContent: 'center', 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /app/src/util/languageTo.ts: -------------------------------------------------------------------------------- 1 | import {LanguageCode} from 'react-native-translator'; 2 | 3 | export const korean = (language: LanguageCode<'google'>) => { 4 | switch (language) { 5 | case 'ko': 6 | return '한국어'; 7 | case 'en': 8 | return '영어'; 9 | case 'ja': 10 | return '일본어'; 11 | case 'zh-CN': 12 | return '중국어 간체'; 13 | case 'zh-TW': 14 | return '중국어 번체'; 15 | case 'vi': 16 | return '베트남어'; 17 | case 'de': 18 | return '독일어'; 19 | case 'es': 20 | return '스페인어'; 21 | case 'fr': 22 | return '프랑스어'; 23 | case 'it': 24 | return '이탈리아어'; 25 | case 'ru': 26 | return '러시아어'; 27 | case 'th': 28 | return '태국어'; 29 | case 'id': 30 | return '인도네시아어'; 31 | default: 32 | return '오류'; 33 | } 34 | }; 35 | 36 | export const ttsLanguage = (language: LanguageCode<'google'>) => { 37 | switch (language) { 38 | case 'ko': 39 | return 'ko-KR'; 40 | case 'en': 41 | return 'en-IE'; 42 | case 'ja': 43 | return 'ja-JP'; 44 | case 'zh-CN': 45 | return 'zh-CN'; 46 | case 'zh-TW': 47 | return 'zh-TW'; 48 | case 'vi': 49 | return 'vi-VI'; 50 | case 'de': 51 | return 'de-DE'; 52 | case 'es': 53 | return 'es-ES'; 54 | case 'fr': 55 | return 'fr-FR'; 56 | case 'it': 57 | return 'it-IT'; 58 | case 'ru': 59 | return 'ru-RU'; 60 | case 'th': 61 | return 'th-TH'; 62 | case 'id': 63 | return 'id-ID'; 64 | default: 65 | return 'en-IE'; 66 | } 67 | }; 68 | 69 | export default { 70 | korean, 71 | ttsLanguage, 72 | }; 73 | -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} -------------------------------------------------------------------------------- /web/src/pages/HomePage/HomePageHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { BREAK_POINT, COLORS } from '../../constants/styles'; 4 | import Icon from '@mdi/react'; 5 | import { mdiApple, mdiGithub, mdiGooglePlay } from '@mdi/js'; 6 | import { 7 | APPSTORE_URL, 8 | GITHUB_URL, 9 | PLAYSTORE_URL, 10 | } from '../../constants/values'; 11 | 12 | const Container = styled.div` 13 | width: 100%; 14 | height: 56px; 15 | background-color: ${COLORS.red}; 16 | flex-direction: row; 17 | align-items: center; 18 | padding: 0px 24px; 19 | @media (max-width: ${BREAK_POINT}) { 20 | padding: 0px 16px; 21 | } 22 | z-index: 99; 23 | `; 24 | 25 | const Title = styled.a` 26 | font-weight: bold; 27 | color: ${COLORS.white}; 28 | flex: 1; 29 | `; 30 | 31 | const HomePageHeader = () => { 32 | return ( 33 | 34 | 3가지 번역기 비교하다 35 | 56 | 57 | ); 58 | }; 59 | 60 | export default HomePageHeader; 61 | -------------------------------------------------------------------------------- /app/android/app/src/main/java/com/koreanthinker/translators/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.koreanthinker.translators; 2 | 3 | import com.facebook.react.ReactActivity; 4 | import com.facebook.react.ReactActivityDelegate; 5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 6 | import com.facebook.react.defaults.DefaultReactActivityDelegate; 7 | import org.devio.rn.splashscreen.SplashScreen; 8 | import android.os.Bundle; 9 | 10 | public class MainActivity extends ReactActivity { 11 | 12 | /** 13 | * Returns the name of the main component registered from JavaScript. This is used to schedule 14 | * rendering of the component. 15 | */ 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | SplashScreen.show(this); 19 | super.onCreate(savedInstanceState); 20 | } 21 | @Override 22 | protected String getMainComponentName() { 23 | return "app"; 24 | } 25 | 26 | /** 27 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link 28 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React 29 | * (aka React 18) with two boolean flags. 30 | */ 31 | @Override 32 | protected ReactActivityDelegate createReactActivityDelegate() { 33 | return new DefaultReactActivityDelegate( 34 | this, 35 | getMainComponentName(), 36 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 37 | DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled 38 | // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18). 39 | DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/screens/HomeScreen/HomeScreenInput.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, StyleSheet, TextInput} from 'react-native'; 2 | import React, {useContext, useRef} from 'react'; 3 | import BaseInput from '../../components/BaseInput'; 4 | import {TranslateContext} from '../../context/TranslateContext'; 5 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 6 | import BorderlessButton from '../../components/BorderlessButton'; 7 | 8 | const HomeScreenInput = () => { 9 | const inputRef = useRef(null); 10 | 11 | const {text, onChangeText, clear, applyClipboard} = 12 | useContext(TranslateContext); 13 | 14 | return ( 15 | inputRef.current?.focus()} 17 | style={styles.container}> 18 | 26 | 29 | {text ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default HomeScreenInput; 40 | 41 | const styles = StyleSheet.create({ 42 | container: { 43 | flexDirection: 'row', 44 | alignItems: 'center', 45 | }, 46 | input: { 47 | width: '100%', 48 | marginVertical: 56, 49 | flex: 1, 50 | }, 51 | claerBtn: { 52 | width: 32, 53 | height: 32, 54 | alignItems: 'center', 55 | justifyContent: 'center', 56 | marginLeft: 16, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://translators.kr", 6 | "dependencies": { 7 | "@emotion/react": "^11.7.1", 8 | "@emotion/styled": "^11.6.0", 9 | "@mdi/js": "^6.5.95", 10 | "@mdi/react": "^1.5.0", 11 | "@testing-library/jest-dom": "^5.16.2", 12 | "@testing-library/react": "^12.1.3", 13 | "@testing-library/user-event": "^13.5.0", 14 | "@types/jest": "^27.4.0", 15 | "@types/jquery": "^3.5.13", 16 | "@types/node": "^16.11.25", 17 | "@types/react": "^17.0.39", 18 | "@types/react-dom": "^17.0.11", 19 | "@typescript-eslint/eslint-plugin": "^5.12.0", 20 | "@typescript-eslint/parser": "^5.12.0", 21 | "eslint": "^8.2.0", 22 | "eslint-config-prettier": "^8.3.0", 23 | "eslint-plugin-import": "^2.25.3", 24 | "eslint-plugin-prettier": "^4.0.0", 25 | "eslint-plugin-react": "^7.28.0", 26 | "eslint-plugin-react-hooks": "^4.3.0", 27 | "prettier": "^2.5.1", 28 | "react": "^17.0.2", 29 | "react-dom": "^17.0.2", 30 | "react-router-dom": "6", 31 | "react-scripts": "5.0.0", 32 | "react-select": "^5.2.2", 33 | "typescript": "^4.5.5", 34 | "web-vitals": "^2.1.4" 35 | }, 36 | "scripts": { 37 | "start": "react-scripts start", 38 | "build": "react-scripts build", 39 | "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 40 | "test": "react-scripts test", 41 | "test:coverage": "npm run test -- --coverage --watchAll=false", 42 | "spec": "react-scripts test" 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } -------------------------------------------------------------------------------- /app/ios/app/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/screens/HomeScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import {Keyboard, ScrollView, StyleSheet, View} from 'react-native'; 2 | import React, {useContext} from 'react'; 3 | import HomeScreenHeader from './HomeScreenHeader'; 4 | import HomeScreenLanguageSelector from './HomeScreenLanguageSelector'; 5 | import {TranslateContext} from '../../context/TranslateContext'; 6 | import {HistoryContext} from '../../context/HistoryContext'; 7 | import HomeScreenRecentCard from './HomeScreenRecentCard'; 8 | import {CardSequenceContext} from '../../context/CardSequenceContext'; 9 | import HomeScreenTranslatedCard from './HomeScreenTranslatedCard'; 10 | import {useSafeAreaInsets} from 'react-native-safe-area-context'; 11 | import HomeScreenInput from './HomeScreenInput'; 12 | 13 | const HomeScreen = () => { 14 | const {scrollViewRef} = useContext(TranslateContext); 15 | const {historys} = useContext(HistoryContext); 16 | const {cardSequence} = useContext(CardSequenceContext); 17 | const {bottom} = useSafeAreaInsets(); 18 | 19 | return ( 20 | 21 | 22 | 23 | 28 | 29 | {cardSequence.map(translatorType => ( 30 | 34 | ))} 35 | {!!historys.length && } 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default HomeScreen; 43 | 44 | const styles = StyleSheet.create({ 45 | container: { 46 | flex: 1, 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /web/src/util/languageTo.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '../constants/types'; 2 | 3 | export const korean = (language: Language) => { 4 | switch (language) { 5 | case 'kr': 6 | return '한국어'; 7 | case 'en': 8 | return '영어'; 9 | case 'jp': 10 | return '일본어'; 11 | case 'cn': 12 | return '중국어'; 13 | case 'vi': 14 | return '베트남어'; 15 | case 'de': 16 | return '독일어'; 17 | case 'es': 18 | return '스페인어'; 19 | case 'fr': 20 | return '프랑스어'; 21 | case 'it': 22 | return '이탈리아어'; 23 | default: 24 | return '오류'; 25 | } 26 | }; 27 | 28 | export const kakaoLanguage = (language: Language) => language; 29 | export const papagoLanguage = (language: Language) => { 30 | switch (language) { 31 | case 'kr': 32 | return 'ko'; 33 | case 'jp': 34 | return 'ja'; 35 | case 'cn': 36 | return 'zh-CN'; 37 | default: 38 | return language; 39 | } 40 | }; 41 | export const googleLanguage = (language: Language) => { 42 | switch (language) { 43 | case 'kr': 44 | return 'ko'; 45 | case 'jp': 46 | return 'ja'; 47 | case 'cn': 48 | return 'zh-CN'; 49 | default: 50 | return language; 51 | } 52 | }; 53 | export const ttsLanguage = (language: Language) => { 54 | switch (language) { 55 | case 'kr': 56 | return 'ko-KR'; 57 | case 'en': 58 | return 'en-IE'; 59 | case 'jp': 60 | return 'ja-JP'; 61 | case 'cn': 62 | return 'zh-CN'; 63 | case 'vi': 64 | return 'vi-VI'; 65 | case 'de': 66 | return 'de-DE'; 67 | case 'es': 68 | return 'es-ES'; 69 | case 'fr': 70 | return 'fr-FR'; 71 | case 'it': 72 | return 'it-IT'; 73 | default: 74 | return 'en-IE'; 75 | } 76 | }; 77 | 78 | export default { 79 | korean, 80 | ttsLanguage, 81 | googleLanguage, 82 | kakaoLanguage, 83 | papagoLanguage, 84 | }; 85 | -------------------------------------------------------------------------------- /web/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Single Page Apps for GitHub Pages 7 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/context/CardSequenceContext.tsx: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import React, { 3 | createContext, 4 | useCallback, 5 | useEffect, 6 | useMemo, 7 | useState, 8 | } from 'react'; 9 | import {TranslatorType} from '../constants/types'; 10 | import {STOREAGE_CARD_SEQUENCE_ID} from '../constants/values'; 11 | 12 | export type CardSequenceContextType = { 13 | cardSequence: TranslatorType[]; 14 | updateCardSequence: (data: TranslatorType[]) => void; 15 | }; 16 | 17 | export const CardSequenceContext = createContext( 18 | {} as any, 19 | ); 20 | 21 | const CardSequenceProvider: React.FC = ({children}) => { 22 | const [cardSequence, setCardSequence] = useState([ 23 | 'google', 24 | 'papago', 25 | 'kakao', 26 | ]); 27 | 28 | useEffect(() => { 29 | // 최초 1회 데이터를 받아와 스테이트에 저장 30 | (async () => { 31 | const data = await AsyncStorage.getItem(STOREAGE_CARD_SEQUENCE_ID); 32 | if (!data) return; 33 | if (JSON.parse(data).includes('naver')) { 34 | // 2.1.0 버전으로 업그레이드하면서 naver -> papago로 바꿨음 35 | // 따라서 기존 유저들의 Local storage를 임의로 변경해줌 36 | await updateCardSequence(cardSequence); 37 | return; 38 | } 39 | setCardSequence(JSON.parse(data)); 40 | })(); 41 | }, []); 42 | 43 | // 순서 변경 44 | const updateCardSequence = useCallback(async (data: TranslatorType[]) => { 45 | setCardSequence(data); 46 | await AsyncStorage.setItem(STOREAGE_CARD_SEQUENCE_ID, JSON.stringify(data)); // storage동기화 47 | }, []); 48 | 49 | const contextValue = useMemo( 50 | () => ({ 51 | cardSequence, 52 | updateCardSequence, 53 | }), 54 | [cardSequence, updateCardSequence], 55 | ); 56 | 57 | return ( 58 | 59 | {children} 60 | 61 | ); 62 | }; 63 | 64 | export default CardSequenceProvider; 65 | -------------------------------------------------------------------------------- /app/src/screens/HomeScreen/HomeScreenRecentCard.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, StyleSheet, Text, View} from 'react-native'; 2 | import React, {useContext} from 'react'; 3 | import {COLORS, SHADOW, WIDTH} from '../../constants/styles'; 4 | import {History} from '../../constants/types'; 5 | import Icon from 'react-native-vector-icons/MaterialIcons'; 6 | import {TranslateContext} from '../../context/TranslateContext'; 7 | import Typography from '../../components/Typography'; 8 | import BorderlessButton from '../../components/BorderlessButton'; 9 | 10 | const HomeScreenRecentCard: React.FC = props => { 11 | const {applyHistory} = useContext(TranslateContext); 12 | const {text} = props; 13 | 14 | return ( 15 | 16 | 최근검색 17 | {text} 18 | 19 | applyHistory(props)} 21 | style={styles.icon}> 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default HomeScreenRecentCard; 30 | 31 | const styles = StyleSheet.create({ 32 | container: { 33 | width: '100%', 34 | minHeight: 160, 35 | borderRadius: 16, 36 | ...SHADOW, 37 | backgroundColor: COLORS.red, 38 | }, 39 | title: { 40 | marginLeft: 16, 41 | marginTop: 16, 42 | color: COLORS.white, 43 | fontSize: 20, 44 | fontWeight: 'bold', 45 | }, 46 | text: { 47 | flex: 1, 48 | fontSize: 16, 49 | color: COLORS.white, 50 | margin: 16, 51 | marginBottom: 8, 52 | }, 53 | footer: { 54 | width: '100%', 55 | height: 48, 56 | flexDirection: 'row', 57 | alignItems: 'center', 58 | justifyContent: 'flex-end', 59 | }, 60 | icon: { 61 | width: 48, 62 | height: 48, 63 | alignItems: 'center', 64 | justifyContent: 'center', 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /app/src/context/HistoryContext.tsx: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import React, { 3 | createContext, 4 | useCallback, 5 | useEffect, 6 | useMemo, 7 | useState, 8 | } from 'react'; 9 | import {History} from '../constants/types'; 10 | import {STOREAGE_HISTORYS_ID} from '../constants/values'; 11 | 12 | export type HistoryContextType = { 13 | historys: History[]; 14 | removeHistory: (id: string) => void; 15 | addHistory: (data: Omit) => void; 16 | }; 17 | 18 | export const HistoryContext = createContext({} as any); 19 | 20 | const HistoryProvider: React.FC = ({children}) => { 21 | const [historys, setHistorys] = useState([]); 22 | 23 | useEffect(() => { 24 | // 최초 1회 데이터를 받아와 historys 스테이트에 저장 25 | (async () => { 26 | const data = await AsyncStorage.getItem(STOREAGE_HISTORYS_ID); 27 | if (!data) return; 28 | setHistorys(JSON.parse(data)); 29 | })(); 30 | }, []); 31 | 32 | useEffect(() => { 33 | // history가 바뀔때마다 storage와 동기화 34 | if (!historys) return; 35 | AsyncStorage.setItem(STOREAGE_HISTORYS_ID, JSON.stringify(historys)); 36 | }, [historys]); 37 | 38 | // 삭제 액션 39 | const removeHistory = useCallback((id: string) => { 40 | setHistorys(prev => prev.filter(value => value.id !== id)); 41 | }, []); 42 | // 추가 액션 43 | const addHistory = useCallback((data: Omit) => { 44 | const history: History = { 45 | id: Date.now().toString(), // 임의의 아이디 생성 46 | ...data, 47 | }; 48 | setHistorys(prev => [history, ...prev.slice(0, 9)]); 49 | }, []); 50 | 51 | const contextValue = useMemo( 52 | () => ({ 53 | historys, 54 | removeHistory, 55 | addHistory, 56 | }), 57 | [historys, removeHistory, addHistory], 58 | ); 59 | 60 | return ( 61 | 62 | {children} 63 | 64 | ); 65 | }; 66 | 67 | export default HistoryProvider; 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![App](https://github.com/krtk-dev/translators/actions/workflows/ci-app.yml/badge.svg)](https://github.com/krtk-dev/translators/actions/workflows/ci-app.yml) 2 | [![Web](https://github.com/krtk-dev/translators/actions/workflows/ci-web.yml/badge.svg)](https://github.com/krtk-dev/translators/actions/workflows/ci-web.yml) 3 | [![Deploy Web](https://github.com/krtk-dev/translators/actions/workflows/cd-web.yml/badge.svg)](https://github.com/krtk-dev/translators/actions/workflows/cd-web.yml) 4 | [![codecov](https://codecov.io/gh/krtk-dev/translators/branch/main/graph/badge.svg)](https://codecov.io/gh/krtk-dev/translators) 5 | [![License GPL3.0](https://img.shields.io/github/license/krtk-dev/translators?style=plat)](LICENSE) 6 | 7 | ![Stars](https://img.shields.io/github/stars/krtk-dev/translators?style=social) 8 | ![Twitter](https://img.shields.io/twitter/follow/koreanthinker?style=social) 9 | 10 | 11 | ![Typescript](https://img.shields.io/badge/Typescript-222222?style=for-the-badge&logo=Typescript&logoColor=#3178C6) 12 | ![React](https://img.shields.io/badge/React-222222?style=for-the-badge&logo=React&logoColor=#61DAFB) 13 | ![ReactNative](https://img.shields.io/badge/ReactNative-222222?style=for-the-badge&logo=React&logoColor=#61DAFB) 14 | ![Jest](https://img.shields.io/badge/Jest-222222?style=for-the-badge&logo=Jest&logoColor=#C21325) 15 | 16 | 17 | 18 | ### Introduction 19 | 20 | `Compare 3 Translators` is a mobile app service that lets you compare Google, Papago, and Kakao Translate. 21 | 22 | ### Product 23 | - App 24 | 25 | 26 | 27 | - Web 28 | 29 | [https://translators.kr](https://translators.kr) (Deprecated) 30 | - UI/UX 31 | 32 | [figma](https://www.figma.com/file/iEKYQOU8kCFyLE8voCKOCY/Translators?node-id=0%3A1) 33 | -------------------------------------------------------------------------------- /app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.125.0 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=false 41 | 42 | # Use this property to enable or disable the Hermes JS engine. 43 | # If set to false, you will be using JSC instead. 44 | hermesEnabled=true 45 | 46 | MYAPP_RELEASE_STORE_FILE=translators_keystore.jks 47 | MYAPP_RELEASE_KEY_ALIAS=koreanthinker 48 | MYAPP_RELEASE_STORE_PASSWORD=247291 49 | MYAPP_RELEASE_KEY_PASSWORD=247291 50 | -------------------------------------------------------------------------------- /app/ios/appTests/appTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface appAppTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation appAppTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /app/ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, min_ios_version_supported 5 | prepare_react_native_project! 6 | 7 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. 8 | # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded 9 | # 10 | # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` 11 | # ```js 12 | # module.exports = { 13 | # dependencies: { 14 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), 15 | # ``` 16 | flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled 17 | 18 | linkage = ENV['USE_FRAMEWORKS'] 19 | if linkage != nil 20 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 21 | use_frameworks! :linkage => linkage.to_sym 22 | end 23 | 24 | target 'app' do 25 | config = use_native_modules! 26 | 27 | # Flags change depending on the env values. 28 | flags = get_default_flags() 29 | 30 | use_react_native!( 31 | :path => config[:reactNativePath], 32 | # Hermes is now enabled by default. Disable by setting this flag to false. 33 | # Upcoming versions of React Native may rely on get_default_flags(), but 34 | # we make it explicit here to aid in the React Native upgrade process. 35 | :hermes_enabled => flags[:hermes_enabled], 36 | :fabric_enabled => flags[:fabric_enabled], 37 | # Enables Flipper. 38 | # 39 | # Note that if you have use_frameworks! enabled, Flipper will not work and 40 | # you should disable the next line. 41 | :flipper_configuration => flipper_config, 42 | # An absolute path to your application root. 43 | :app_path => "#{Pod::Config.instance.installation_root}/.." 44 | ) 45 | 46 | target 'appTests' do 47 | inherit! :complete 48 | # Pods for testing 49 | end 50 | 51 | post_install do |installer| 52 | react_native_post_install( 53 | installer, 54 | # Set `mac_catalyst_enabled` to `true` in order to apply patches 55 | # necessary for Mac Catalyst builds 56 | :mac_catalyst_enabled => false 57 | ) 58 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/android/app/src/main/java/com/koreanthinker/translators/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.koreanthinker.translators; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 11 | import com.facebook.react.defaults.DefaultReactNativeHost; 12 | import com.facebook.soloader.SoLoader; 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.List; 15 | import com.facebook.react.bridge.JSIModulePackage; 16 | 17 | 18 | 19 | public class MainApplication extends Application implements ReactApplication { 20 | 21 | 22 | private final ReactNativeHost mReactNativeHost = 23 | new DefaultReactNativeHost(this) { 24 | @Override 25 | public boolean getUseDeveloperSupport() { 26 | return BuildConfig.DEBUG; 27 | } 28 | 29 | @Override 30 | protected List getPackages() { 31 | @SuppressWarnings("UnnecessaryLocalVariable") 32 | List packages = new PackageList(this).getPackages(); 33 | // Packages that cannot be autolinked yet can be added manually here, for example: 34 | // packages.add(new MyReactNativePackage()); 35 | return packages; 36 | } 37 | 38 | @Override 39 | protected String getJSMainModuleName() { 40 | return "index"; 41 | } 42 | 43 | @Override 44 | protected boolean isNewArchEnabled() { 45 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 46 | } 47 | @Override 48 | protected Boolean isHermesEnabled() { 49 | return BuildConfig.IS_HERMES_ENABLED; 50 | } 51 | }; 52 | 53 | @Override 54 | public ReactNativeHost getReactNativeHost() { 55 | return mReactNativeHost; 56 | } 57 | 58 | @Override 59 | public void onCreate() { 60 | super.onCreate(); 61 | SoLoader.init(this, /* native exopackage */ false); 62 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 63 | // If you opted-in for the New Architecture, we load the native entry point for this app. 64 | DefaultNewArchitectureEntryPoint.load(); 65 | } 66 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/ios/app/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ko_KR 7 | CFBundleDisplayName 8 | 3가지 번역기 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | ITSAppUsesNonExemptEncryption 26 | 27 | LSRequiresIPhoneOS 28 | 29 | NSAppTransportSecurity 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UIAppFonts 43 | 44 | AntDesign.ttf 45 | Entypo.ttf 46 | EvilIcons.ttf 47 | Feather.ttf 48 | FontAwesome.ttf 49 | FontAwesome5_Brands.ttf 50 | FontAwesome5_Regular.ttf 51 | FontAwesome5_Solid.ttf 52 | Foundation.ttf 53 | Ionicons.ttf 54 | MaterialIcons.ttf 55 | MaterialCommunityIcons.ttf 56 | SimpleLineIcons.ttf 57 | Octicons.ttf 58 | Zocial.ttf 59 | Fontisto.ttf 60 | 61 | UILaunchStoryboardName 62 | LaunchScreen.storyboard 63 | UIMainStoryboardFile 64 | LaunchScreen 65 | UIRequiredDeviceCapabilities 66 | 67 | armv7 68 | 69 | UISupportedInterfaceOrientations 70 | 71 | UIInterfaceOrientationPortrait 72 | 73 | UIViewControllerBasedStatusBarAppearance 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /web/src/pages/HomePage/HomePageTranslatedCard.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { mdiContentCopy, mdiSwapHorizontalVariant } from '@mdi/js'; 3 | import Icon from '@mdi/react'; 4 | import React, { useContext } from 'react'; 5 | import { BREAK_POINT, COLORS } from '../../constants/styles'; 6 | import { Translator } from '../../constants/types'; 7 | import { TranslateContext } from '../../context/TranslateContext'; 8 | 9 | const Container = styled.div` 10 | box-shadow: 0px 4px 4px #cccccc; 11 | border-radius: 16px; 12 | min-height: 200px; 13 | max-width: 600px; 14 | width: 100%; 15 | margin-bottom: 16px; 16 | @media (max-width: ${BREAK_POINT}) { 17 | min-height: 160px; 18 | max-width: ${BREAK_POINT}; 19 | } 20 | `; 21 | 22 | const Title = styled.div` 23 | color: ${COLORS.white}; 24 | font-weight: bold; 25 | margin-top: 16px; 26 | margin-left: 16px; 27 | font-size: 18px; 28 | `; 29 | 30 | const Content = styled.div` 31 | flex: 1; 32 | color: ${COLORS.white}; 33 | margin-top: 8px; 34 | margin-left: 16px; 35 | margin-right: 16px; 36 | `; 37 | 38 | const Footer = styled.div` 39 | height: 56px; 40 | flex-direction: row; 41 | align-items: center; 42 | justify-content: flex-end; 43 | `; 44 | 45 | const IconButton = styled.div` 46 | width: 56px; 47 | height: 56px; 48 | align-items: center; 49 | justify-content: center; 50 | margin-left: -8px; 51 | &:hover { 52 | cursor: pointer; 53 | } 54 | `; 55 | 56 | interface HomePageTranslatedCardProps { 57 | translator: Translator; 58 | } 59 | 60 | const HomePageTranslatedCard: React.FC = ({ 61 | translator, 62 | }) => { 63 | const { translatedData, reverseTranslate } = useContext(TranslateContext); 64 | 65 | const content = translatedData[translator]; 66 | 67 | return ( 68 | 72 | {translator.toUpperCase()} 73 | {content} 74 |
75 | reverseTranslate(content)}> 76 | 81 | 82 | navigator.clipboard.writeText(content)}> 83 | 84 | 85 |
86 |
87 | ); 88 | }; 89 | 90 | export default HomePageTranslatedCard; 91 | -------------------------------------------------------------------------------- /app/src/screens/HomeDrawerScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, Text, View} from 'react-native'; 2 | import React from 'react'; 3 | import {COLORS, STATUSBAR_HEIGHT} from '../../constants/styles'; 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | import BaseButton from '../../components/BaseButton'; 6 | import Typography from '../../components/Typography'; 7 | import useNavigation from '../../hooks/useNavigation'; 8 | 9 | const HomeDrawerScreen = () => { 10 | const {navigate} = useNavigation(); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | navigate('CardSequence')} 21 | style={styles.tabContainer}> 22 | 28 | 카드 순서변경 29 | 30 | navigate('History')} 32 | style={styles.tabContainer}> 33 | 39 | 번역기록 40 | 41 | navigate('Credit')} 43 | style={styles.tabContainer}> 44 | 50 | 크레딧 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default HomeDrawerScreen; 57 | 58 | const styles = StyleSheet.create({ 59 | container: { 60 | flex: 1, 61 | backgroundColor: COLORS.white, 62 | }, 63 | header: { 64 | width: '100%', 65 | height: 120 + STATUSBAR_HEIGHT, 66 | paddingTop: STATUSBAR_HEIGHT, 67 | backgroundColor: COLORS.red, 68 | justifyContent: 'center', 69 | paddingHorizontal: 24, 70 | marginBottom: 8, 71 | }, 72 | iconContainer: { 73 | width: 80, 74 | height: 80, 75 | alignItems: 'center', 76 | justifyContent: 'center', 77 | borderRadius: 40, 78 | backgroundColor: COLORS.white, 79 | }, 80 | tabContainer: { 81 | width: '100%', 82 | height: 48, 83 | flexDirection: 'row', 84 | alignItems: 'center', 85 | paddingHorizontal: 16, 86 | }, 87 | }); 88 | -------------------------------------------------------------------------------- /app/src/screens/HistoryScreen/HistoryScreenHistoryCard.tsx: -------------------------------------------------------------------------------- 1 | import {Pressable, StyleSheet, Text, View} from 'react-native'; 2 | import React, {useContext} from 'react'; 3 | import {COLORS, SHADOW, WIDTH} from '../../constants/styles'; 4 | import {History} from '../../constants/types'; 5 | import languageTo from '../../util/languageTo'; 6 | import useNavigation from '../../hooks/useNavigation'; 7 | import Icon from 'react-native-vector-icons/MaterialIcons'; 8 | import {TranslateContext} from '../../context/TranslateContext'; 9 | import {HistoryContext} from '../../context/HistoryContext'; 10 | import Typography from '../../components/Typography'; 11 | import BorderlessButton from '../../components/BorderlessButton'; 12 | 13 | const HistoryScreenHistoryCard: React.FC = props => { 14 | const {navigate} = useNavigation(); 15 | const {applyHistory} = useContext(TranslateContext); 16 | const {removeHistory} = useContext(HistoryContext); 17 | const {id, text, toLanguage} = props; 18 | 19 | return ( 20 | 21 | 22 | {`${languageTo.korean(toLanguage)}로`} 23 | 24 | {text} 25 | 26 | removeHistory(id)} style={styles.icon}> 27 | 28 | 29 | { 31 | applyHistory(props); 32 | navigate('Home'); 33 | }} 34 | style={styles.icon}> 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default HistoryScreenHistoryCard; 43 | 44 | const styles = StyleSheet.create({ 45 | container: { 46 | width: '100%', 47 | minHeight: 160, 48 | borderRadius: 16, 49 | marginBottom: 16, 50 | ...SHADOW, 51 | backgroundColor: COLORS.red, 52 | }, 53 | title: { 54 | marginLeft: 16, 55 | marginTop: 16, 56 | color: COLORS.white, 57 | fontSize: 20, 58 | fontWeight: 'bold', 59 | }, 60 | text: { 61 | flex: 1, 62 | fontSize: 16, 63 | color: COLORS.white, 64 | margin: 16, 65 | marginBottom: 8, 66 | }, 67 | footer: { 68 | width: '100%', 69 | height: 48, 70 | flexDirection: 'row', 71 | alignItems: 'center', 72 | justifyContent: 'flex-end', 73 | }, 74 | icon: { 75 | marginLeft: -4, 76 | width: 48, 77 | height: 48, 78 | alignItems: 'center', 79 | justifyContent: 'center', 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | activesupport (7.0.4.2) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | addressable (2.8.1) 12 | public_suffix (>= 2.0.2, < 6.0) 13 | algoliasearch (1.27.5) 14 | httpclient (~> 2.8, >= 2.8.3) 15 | json (>= 1.5.1) 16 | atomos (0.1.3) 17 | claide (1.1.0) 18 | cocoapods (1.12.0) 19 | addressable (~> 2.8) 20 | claide (>= 1.0.2, < 2.0) 21 | cocoapods-core (= 1.12.0) 22 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 23 | cocoapods-downloader (>= 1.6.0, < 2.0) 24 | cocoapods-plugins (>= 1.0.0, < 2.0) 25 | cocoapods-search (>= 1.0.0, < 2.0) 26 | cocoapods-trunk (>= 1.6.0, < 2.0) 27 | cocoapods-try (>= 1.1.0, < 2.0) 28 | colored2 (~> 3.1) 29 | escape (~> 0.0.4) 30 | fourflusher (>= 2.3.0, < 3.0) 31 | gh_inspector (~> 1.0) 32 | molinillo (~> 0.8.0) 33 | nap (~> 1.0) 34 | ruby-macho (>= 2.3.0, < 3.0) 35 | xcodeproj (>= 1.21.0, < 2.0) 36 | cocoapods-core (1.12.0) 37 | activesupport (>= 5.0, < 8) 38 | addressable (~> 2.8) 39 | algoliasearch (~> 1.0) 40 | concurrent-ruby (~> 1.1) 41 | fuzzy_match (~> 2.0.4) 42 | nap (~> 1.0) 43 | netrc (~> 0.11) 44 | public_suffix (~> 4.0) 45 | typhoeus (~> 1.0) 46 | cocoapods-deintegrate (1.0.5) 47 | cocoapods-downloader (1.6.3) 48 | cocoapods-plugins (1.0.0) 49 | nap 50 | cocoapods-search (1.0.1) 51 | cocoapods-trunk (1.6.0) 52 | nap (>= 0.8, < 2.0) 53 | netrc (~> 0.11) 54 | cocoapods-try (1.2.0) 55 | colored2 (3.1.2) 56 | concurrent-ruby (1.2.2) 57 | escape (0.0.4) 58 | ethon (0.16.0) 59 | ffi (>= 1.15.0) 60 | ffi (1.15.5) 61 | fourflusher (2.3.1) 62 | fuzzy_match (2.0.4) 63 | gh_inspector (1.1.3) 64 | httpclient (2.8.3) 65 | i18n (1.12.0) 66 | concurrent-ruby (~> 1.0) 67 | json (2.6.3) 68 | minitest (5.18.0) 69 | molinillo (0.8.0) 70 | nanaimo (0.3.0) 71 | nap (1.1.0) 72 | netrc (0.11.0) 73 | public_suffix (4.0.7) 74 | rexml (3.2.5) 75 | ruby-macho (2.5.1) 76 | typhoeus (1.4.0) 77 | ethon (>= 0.9.0) 78 | tzinfo (2.0.6) 79 | concurrent-ruby (~> 1.0) 80 | xcodeproj (1.22.0) 81 | CFPropertyList (>= 2.3.3, < 4.0) 82 | atomos (~> 0.1.3) 83 | claide (>= 1.0.2, < 2.0) 84 | colored2 (~> 3.1) 85 | nanaimo (~> 0.3.0) 86 | rexml (~> 3.2.4) 87 | 88 | PLATFORMS 89 | ruby 90 | 91 | DEPENDENCIES 92 | cocoapods (~> 1.11, >= 1.11.3) 93 | 94 | RUBY VERSION 95 | ruby 2.7.6p219 96 | 97 | BUNDLED WITH 98 | 2.1.4 99 | -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | 3가지 번역기 비교하다 26 | 27 | 28 | 29 | 51 | 52 | 53 | 54 | 55 |
56 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/screens/CreditScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, Linking, ToastAndroid, StyleSheet} from 'react-native'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | import BaseButton from '../../components/BaseButton'; 5 | import {COLORS, STATUSBAR_HEIGHT} from '../../constants/styles'; 6 | import useNavigation from '../../hooks/useNavigation'; 7 | import DeviceInfo from 'react-native-device-info'; 8 | import InAppReview from 'react-native-in-app-review'; 9 | 10 | const CreditScreen = () => { 11 | const {goBack, navigate} = useNavigation(); 12 | 13 | return ( 14 | 15 | Linking.openURL('mailto:koreanthinker@gmail.com')} 17 | style={[styles.itemContainer, {marginTop: 16}]}> 18 | 19 | 20 | 21 | coderhyun476@gmail.com 22 | 23 | Linking.openURL('https://www.github.com/koreanthinker')} 25 | style={styles.itemContainer}> 26 | 27 | 28 | 29 | github.com/koreanthinker 30 | 31 | navigate('Oss')} style={styles.itemContainer}> 32 | 33 | 34 | 35 | open source librarys 36 | 37 | InAppReview.RequestInAppReview()} 39 | style={styles.itemContainer}> 40 | 41 | 42 | 43 | rate us 44 | 45 | 46 | 47 | 48 | 49 | {DeviceInfo.getVersion()} 50 | 51 | goBack()} style={styles.itemContainer}> 52 | 53 | 54 | 55 | 뒤로 56 | 57 | 58 | ); 59 | }; 60 | 61 | const styles = StyleSheet.create({ 62 | container: { 63 | backgroundColor: COLORS.red, 64 | paddingTop: STATUSBAR_HEIGHT, 65 | flex: 1, 66 | }, 67 | itemContainer: { 68 | width: '100%', 69 | height: 56, 70 | flexDirection: 'row', 71 | alignItems: 'center', 72 | }, 73 | iconContainer: { 74 | width: 56, 75 | alignItems: 'center', 76 | }, 77 | text: { 78 | fontSize: 16, 79 | color: COLORS.white, 80 | }, 81 | }); 82 | 83 | export default CreditScreen; 84 | -------------------------------------------------------------------------------- /web/src/pages/HomePage/HomePageLanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { LANGUAGES } from '../../constants/values'; 3 | import { TranslateContext } from '../../context/TranslateContext'; 4 | import languageTo from '../../util/languageTo'; 5 | import styled from '@emotion/styled'; 6 | import { BREAK_POINT, COLORS } from '../../constants/styles'; 7 | import { mdiCompareHorizontal, mdiMenuDown } from '@mdi/js'; 8 | import Icon from '@mdi/react'; 9 | import { Language } from '../../constants/types'; 10 | 11 | const Container = styled.div` 12 | width: 100%; 13 | height: 48px; 14 | flex-direction: row; 15 | background-color: ${COLORS.white}; 16 | box-shadow: 0px 4px 4px #cccccc; 17 | `; 18 | 19 | const Devider = styled.div` 20 | flex: 1; 21 | @media (max-width: ${BREAK_POINT}) { 22 | flex: 0; 23 | } 24 | `; 25 | 26 | const SelectorContainer = styled.div` 27 | flex-direction: row; 28 | align-items: center; 29 | flex: 1; 30 | `; 31 | 32 | const LanguageContainer = styled.div` 33 | flex-direction: row; 34 | align-items: center; 35 | justify-content: center; 36 | flex: 1; 37 | `; 38 | 39 | const LanguageSelect = styled.select` 40 | appearance: none; 41 | background: ${mdiMenuDown}; 42 | font-size: 14px; 43 | border: none; 44 | outline-color: transparent; 45 | &:hover { 46 | cursor: pointer; 47 | } 48 | `; 49 | 50 | const ReverseButton = styled.div` 51 | &:hover { 52 | cursor: pointer; 53 | } 54 | `; 55 | 56 | const HomePageLanguageSelector = () => { 57 | const { 58 | fromLanguage, 59 | toLanguage, 60 | updateFromLanguage, 61 | updateToLanguage, 62 | reverseLanguage, 63 | } = useContext(TranslateContext); 64 | 65 | return ( 66 | 67 | 68 | 69 | updateFromLanguage(e.target.value as Language)} 71 | value={fromLanguage} 72 | > 73 | {LANGUAGES.map(lang => ( 74 | 77 | ))} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | updateToLanguage(e.target.value as Language)} 87 | value={toLanguage} 88 | > 89 | {LANGUAGES.map(lang => ( 90 | 93 | ))} 94 | 95 | 96 | 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | export default HomePageLanguageSelector; 104 | -------------------------------------------------------------------------------- /app/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/android/app/src/debug/java/com/app/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.koreanthinker.translators; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 21 | import com.facebook.react.ReactInstanceEventListener; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | /** 28 | * Class responsible of loading Flipper inside your React Native application. This is the debug 29 | * flavor of it. Here you can add your own plugins and customize the Flipper setup. 30 | */ 31 | public class ReactNativeFlipper { 32 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 33 | if (FlipperUtils.shouldEnableFlipper(context)) { 34 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 35 | 36 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 37 | client.addPlugin(new DatabasesFlipperPlugin(context)); 38 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 39 | client.addPlugin(CrashReporterPlugin.getInstance()); 40 | 41 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 42 | NetworkingModule.setCustomClientBuilder( 43 | new NetworkingModule.CustomClientBuilder() { 44 | @Override 45 | public void apply(OkHttpClient.Builder builder) { 46 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 47 | } 48 | }); 49 | client.addPlugin(networkFlipperPlugin); 50 | client.start(); 51 | 52 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 53 | // Hence we run if after all native modules have been initialized 54 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 55 | if (reactContext == null) { 56 | reactInstanceManager.addReactInstanceEventListener( 57 | new ReactInstanceEventListener() { 58 | @Override 59 | public void onReactContextInitialized(ReactContext reactContext) { 60 | reactInstanceManager.removeReactInstanceEventListener(this); 61 | reactContext.runOnNativeModulesQueueThread( 62 | new Runnable() { 63 | @Override 64 | public void run() { 65 | client.addPlugin(new FrescoFlipperPlugin()); 66 | } 67 | }); 68 | } 69 | }); 70 | } else { 71 | client.addPlugin(new FrescoFlipperPlugin()); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "2.1.5", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-native start", 7 | "android": "adb devices && react-native run-android", 8 | "ios": "react-native run-ios", 9 | "android:release": "adb devices && react-native run-android --variant=release", 10 | "ios:device": "react-native run-ios --device \"Namgunghyun13mini\"", 11 | "ios:release": "react-native run-ios --configuration Release", 12 | "ios:device:release": "react-native run-ios --device \"Namgunghyun13mini\" --configuration Release", 13 | "android:assembleRelease": "cd android && ./gradlew assembleRelease", 14 | "android:bundleRelease": "cd android && ./gradlew bundleRelease", 15 | "gradlew:clean": "cd android && ./gradlew clean && cd ..", 16 | "node:clean": "rm -rf node_modules && rm yarn.lock", 17 | "node:clean:force": "yarn cache clean", 18 | "android:clean": "rm -rf android/build && sudo rm -rf android/app/build && sudo rm -rf android/.gradle", 19 | "pod": "cd ios && pod install && cd ..", 20 | "pod:clean": "cd ios && rm -rf Pods && rm Podfile.lock && pod deintegrate && pod setup && pod repo update && cd ..", 21 | "pod:clean:force": "sudo rm -rf ~/Library/Caches/CocoaPods", 22 | "ios:clean": "cd ios && xcodebuild clean", 23 | "ios:clean:force": "rm -rf ~/Library/Developer/Xcode/DerivedData/*", 24 | "test": "NODE_ENV=test jest", 25 | "test:coverage": "npm run test -- --coverage --watchAll=false", 26 | "lint": "eslint .", 27 | "postversion": "react-native-version --never-amend", 28 | "oss": "npx license-checker --json --out ./src/assets/oss.json" 29 | }, 30 | "dependencies": { 31 | "@react-native-async-storage/async-storage": "^1.16.1", 32 | "@react-native-community/clipboard": "^1.5.1", 33 | "@react-native-community/hooks": "^2.8.1", 34 | "@react-navigation/drawer": "^6.3.1", 35 | "@react-navigation/native": "^6.0.8", 36 | "@react-navigation/stack": "^6.1.1", 37 | "react": "18.2.0", 38 | "react-native": "0.71.4", 39 | "react-native-auto-size-text": "^1.1.1", 40 | "react-native-device-info": "^8.4.9", 41 | "react-native-draggable-flatlist": "^3.0.5", 42 | "react-native-gesture-handler": "^2.9.0", 43 | "react-native-in-app-review": "^3.3.2", 44 | "react-native-material-menu": "^2.0.0", 45 | "react-native-reanimated": "^2.14.4", 46 | "react-native-safe-area-context": "^3.3.2", 47 | "react-native-screens": "^3.20.0", 48 | "react-native-splash-screen": "^3.3.0", 49 | "react-native-status-bar-height": "^2.6.0", 50 | "react-native-translator": "1.1.6", 51 | "react-native-tts": "^4.1.0", 52 | "react-native-vector-icons": "^9.2.0", 53 | "react-native-webview": "^11.26.1" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.20.0", 57 | "@babel/preset-env": "^7.20.0", 58 | "@babel/runtime": "^7.20.0", 59 | "@react-native-community/eslint-config": "^3.2.0", 60 | "@tsconfig/react-native": "^2.0.2", 61 | "@types/jest": "^29.2.1", 62 | "@types/react": "^18.0.24", 63 | "@types/react-native-vector-icons": "^6.4.10", 64 | "@types/react-test-renderer": "^18.0.0", 65 | "babel-jest": "^29.2.1", 66 | "eslint": "^8.19.0", 67 | "eslint-plugin-ft-flow": "^2.0.3", 68 | "jest": "^29.2.1", 69 | "metro-react-native-babel-preset": "0.73.8", 70 | "prettier": "^2.4.1", 71 | "react-test-renderer": "18.2.0", 72 | "typescript": "4.8.4" 73 | }, 74 | "resolutions": { 75 | "@types/react": "^17" 76 | }, 77 | "jest": { 78 | "preset": "react-native", 79 | "moduleFileExtensions": [ 80 | "ts", 81 | "tsx", 82 | "js", 83 | "jsx", 84 | "json", 85 | "node" 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/screens/HomeScreen/HomeScreenLanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityIndicator, 3 | Pressable, 4 | StyleSheet, 5 | Text, 6 | View, 7 | } from 'react-native'; 8 | import React, {useContext, useState} from 'react'; 9 | import {COLORS, SHADOW} from '../../constants/styles'; 10 | import Typography from '../../components/Typography'; 11 | import {TranslateContext} from '../../context/TranslateContext'; 12 | import languageTo from '../../util/languageTo'; 13 | import Icon from 'react-native-vector-icons/MaterialIcons'; 14 | import BorderlessButton from '../../components/BorderlessButton'; 15 | import {Menu, MenuItem} from 'react-native-material-menu'; 16 | import {LANGUAGES_CODES} from '../../constants/values'; 17 | import {LanguageCode} from 'react-native-translator'; 18 | 19 | const HomeScreenLanguageSelector = () => { 20 | const { 21 | toLanguage, 22 | fromLanguage, 23 | reverseLanguage, 24 | updateToLanguage, 25 | updateFromLanguage, 26 | } = useContext(TranslateContext); 27 | 28 | const [fromMenuVisible, setFromMenuVisible] = useState(false); 29 | const [toMenuVisible, setToMenuVisible] = useState(false); 30 | 31 | return ( 32 | 33 | setFromMenuVisible(true)} 35 | style={styles.languageButton}> 36 | setFromMenuVisible(false)} 39 | onSelect={updateFromLanguage}> 40 | 41 | {languageTo.korean(fromLanguage)} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | setToMenuVisible(true)} 53 | style={styles.languageButton}> 54 | setToMenuVisible(false)} 57 | onSelect={updateToLanguage}> 58 | 59 | {languageTo.korean(toLanguage)} 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | interface LanguageSelectMenu { 69 | visible: boolean; 70 | onRequestClose: () => void; 71 | onSelect: (language: LanguageCode<'google'>) => void; 72 | } 73 | 74 | const LanguageSelectMenu: React.FC = ({ 75 | visible, 76 | onRequestClose, 77 | onSelect, 78 | children, 79 | }) => { 80 | return ( 81 |

82 | {LANGUAGES_CODES.map(language => ( 83 | { 86 | onSelect(language); 87 | onRequestClose(); 88 | }}> 89 | {languageTo.korean(language)} 90 | 91 | ))} 92 | 93 | ); 94 | }; 95 | 96 | export default HomeScreenLanguageSelector; 97 | 98 | const styles = StyleSheet.create({ 99 | container: { 100 | width: '100%', 101 | height: 48, 102 | flexDirection: 'row', 103 | ...SHADOW, 104 | backgroundColor: COLORS.white, 105 | }, 106 | languageButton: { 107 | flex: 1, 108 | height: '100%', 109 | justifyContent: 'center', 110 | }, 111 | languageContainer: { 112 | flexDirection: 'row', 113 | alignItems: 'center', 114 | justifyContent: 'center', 115 | height: '100%', 116 | }, 117 | reverseButton: { 118 | width: 48, 119 | height: 48, 120 | alignItems: 'center', 121 | justifyContent: 'center', 122 | }, 123 | }); 124 | -------------------------------------------------------------------------------- /web/src/context/TranslateContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useCallback, 4 | useEffect, 5 | useMemo, 6 | useState, 7 | } from 'react'; 8 | import { Language } from '../constants/types'; 9 | import languageTo from '../util/languageTo'; 10 | 11 | export interface TranslatedData { 12 | google: string; 13 | kakao: string; 14 | papago: string; 15 | } 16 | 17 | export type TranslateContextType = { 18 | text: string; 19 | fromLanguage: Language; 20 | toLanguage: Language; 21 | translatedData: TranslatedData; 22 | onChangeText: (text: string) => void; 23 | reverseTranslate: (text: string) => void; 24 | updateFromLanguage: (language: Language) => void; 25 | updateToLanguage: (language: Language) => void; 26 | reverseLanguage: () => void; 27 | }; 28 | 29 | export const TranslateContext = createContext( 30 | {} as TranslateContextType, 31 | ); 32 | 33 | const TranslateProvider: React.FC = ({ children }) => { 34 | const [text, setText] = useState(''); 35 | const [translatedData, setTranslatedData] = useState({ 36 | google: '', 37 | kakao: '', 38 | papago: '', 39 | }); 40 | const [fromLanguage, setFromLanguage] = useState('kr'); 41 | const [toLanguage, setToLanguage] = useState('en'); 42 | 43 | useEffect(() => { 44 | if (text) { 45 | setTranslatedData({ 46 | google: '로딩중...', 47 | kakao: '로딩중...', 48 | papago: '로딩중...', 49 | }); 50 | } else { 51 | setTranslatedData({ 52 | google: '', 53 | kakao: '', 54 | papago: '', 55 | }); 56 | } 57 | }, [text]); 58 | 59 | const reverseLanguage = useCallback(() => { 60 | // 원문과 번역할 언어를 서로 바꿈 61 | setFromLanguage(toLanguage); 62 | setToLanguage(fromLanguage); 63 | }, [toLanguage, fromLanguage]); 64 | 65 | const reverseTranslate = useCallback( 66 | (_text: string) => { 67 | setText(_text); // text를 적용하고 68 | reverseLanguage(); // 언어도 바꿈 69 | }, 70 | [reverseLanguage], 71 | ); 72 | 73 | const updateFromLanguage = useCallback( 74 | // 원문 언어 변경 75 | (language: Language) => { 76 | if (toLanguage === language) setToLanguage(fromLanguage); // 같은 언어 끼리 번역 방지 77 | setFromLanguage(language); 78 | }, 79 | [toLanguage, fromLanguage], 80 | ); 81 | 82 | const updateToLanguage = useCallback( 83 | // 변역 할 언어 변경 84 | (language: Language) => { 85 | if (fromLanguage === language) setFromLanguage(toLanguage); // 같은 언어 끼리 번역 방지 86 | setToLanguage(language); 87 | }, 88 | [toLanguage, fromLanguage], 89 | ); 90 | 91 | const onTranslated = useCallback((data: TranslatedData) => { 92 | setTranslatedData(data); 93 | }, []); 94 | 95 | const contextValue = useMemo( 96 | () => ({ 97 | text, 98 | onChangeText: t => setText(t), 99 | fromLanguage, 100 | toLanguage, 101 | translatedText: translatedData, 102 | reverseLanguage, 103 | reverseTranslate, 104 | updateFromLanguage, 105 | updateToLanguage, 106 | translatedData, 107 | }), 108 | [ 109 | fromLanguage, 110 | reverseLanguage, 111 | reverseTranslate, 112 | text, 113 | toLanguage, 114 | translatedData, 115 | updateFromLanguage, 116 | updateToLanguage, 117 | ], 118 | ); 119 | // -------------- mock data -------------- // 120 | const [timer, setTimer] = useState(null); 121 | useEffect(() => { 122 | timer && clearTimeout(timer); 123 | if (!text) return; 124 | const _timer = setTimeout(() => { 125 | onTranslated({ 126 | google: `번역완료!`, 127 | papago: `(${text})를(을) ${languageTo.korean( 128 | toLanguage, 129 | )}로 번역한 결과는`, 130 | kakao: '오른쪽 상단 앱스토어 버튼을 눌러, 앱애서 확인해주세요!', 131 | }); 132 | }, 1000); 133 | setTimer(_timer); 134 | }, [text]); 135 | // -------------- mock data -------------- // 136 | 137 | return ( 138 | 139 | {children} 140 | 141 | ); 142 | }; 143 | 144 | export default TranslateProvider; 145 | -------------------------------------------------------------------------------- /app/src/screens/HomeScreen/HomeScreenTranslatedCard.tsx: -------------------------------------------------------------------------------- 1 | import {Share, StyleSheet, Text, View} from 'react-native'; 2 | import React, {useCallback, useContext, useState} from 'react'; 3 | import {TranslateContext} from '../../context/TranslateContext'; 4 | import {COLORS, SHADOW} from '../../constants/styles'; 5 | import Typography from '../../components/Typography'; 6 | import BorderlessButton from '../../components/BorderlessButton'; 7 | import Icon from 'react-native-vector-icons/MaterialIcons'; 8 | import Clipboard from '@react-native-community/clipboard'; 9 | import {Menu, MenuItem} from 'react-native-material-menu'; 10 | import useNavigation from '../../hooks/useNavigation'; 11 | import Tts from 'react-native-tts'; 12 | import Translator, { 13 | languageCodeConverter, 14 | TranslatorType, 15 | } from 'react-native-translator'; 16 | 17 | interface HomeScreenTranslatedCardProps { 18 | translatorType: TranslatorType; 19 | } 20 | 21 | const HomeScreenTranslatedCard: React.FC = ({ 22 | translatorType, 23 | }) => { 24 | const {navigate} = useNavigation(); 25 | const {reverseTranslate, fromLanguage, toLanguage, text} = 26 | useContext(TranslateContext); 27 | const [moreVisible, setMoreVisible] = useState(false); 28 | const [result, setResult] = useState(''); 29 | 30 | const onTTS = useCallback(async () => { 31 | await Tts.stop(); 32 | Tts.speak(result); 33 | }, [result]); 34 | 35 | return ( 36 | 37 | setResult(t)} 41 | from={ 42 | languageCodeConverter('google', translatorType, fromLanguage) || 'en' 43 | } 44 | to={languageCodeConverter('google', translatorType, toLanguage) || 'ko'} 45 | /> 46 | 47 | {translatorType.toUpperCase()} 48 | 49 | 50 | {result} 51 | 52 | 53 | 54 | 55 | 56 | Clipboard.setString(result)} 58 | style={styles.icon}> 59 | 60 | 61 | setMoreVisible(true)} 66 | style={styles.icon}> 67 | 68 | 69 | } 70 | onRequestClose={() => setMoreVisible(false)}> 71 | { 73 | setMoreVisible(false); 74 | setTimeout(() => { 75 | Share.share({message: result}); 76 | }, 1000); 77 | }}> 78 | 공유 79 | 80 | { 82 | setMoreVisible(false); 83 | navigate('Full', { 84 | color: COLORS[translatorType], 85 | content: result, 86 | }); 87 | }}> 88 | 전체화면 89 | 90 | { 92 | setMoreVisible(false); 93 | reverseTranslate(result); 94 | }}> 95 | 역번역 96 | 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | export default HomeScreenTranslatedCard; 104 | 105 | const styles = StyleSheet.create({ 106 | container: { 107 | width: '100%', 108 | minHeight: 160, 109 | borderRadius: 16, 110 | ...SHADOW, 111 | marginBottom: 16, 112 | }, 113 | title: { 114 | marginLeft: 16, 115 | marginTop: 16, 116 | color: COLORS.white, 117 | fontSize: 20, 118 | fontWeight: 'bold', 119 | }, 120 | result: { 121 | flex: 1, 122 | fontSize: 16, 123 | color: COLORS.white, 124 | margin: 16, 125 | marginBottom: 8, 126 | }, 127 | footer: { 128 | width: '100%', 129 | height: 48, 130 | flexDirection: 'row', 131 | alignItems: 'center', 132 | justifyContent: 'flex-end', 133 | }, 134 | icon: { 135 | width: 48, 136 | height: 48, 137 | alignItems: 'center', 138 | justifyContent: 'center', 139 | marginLeft: -4, 140 | }, 141 | }); 142 | -------------------------------------------------------------------------------- /app/src/context/TranslateContext.tsx: -------------------------------------------------------------------------------- 1 | import Clipboard from '@react-native-community/clipboard'; 2 | import React, { 3 | createContext, 4 | useCallback, 5 | useContext, 6 | useEffect, 7 | useMemo, 8 | useRef, 9 | useState, 10 | } from 'react'; 11 | import {ScrollView} from 'react-native'; 12 | import InAppReview from 'react-native-in-app-review'; 13 | import {LanguageCode} from 'react-native-translator'; 14 | import Tts from 'react-native-tts'; 15 | import {History} from '../constants/types'; 16 | import languageTo from '../util/languageTo'; 17 | import {HistoryContext} from './HistoryContext'; 18 | 19 | export interface TranslatedData { 20 | google: string | null | Error; 21 | kakao: string | null | Error; 22 | papago: string | null | Error; 23 | } 24 | 25 | export type TranslateContextType = { 26 | scrollViewRef: React.RefObject; 27 | text: string; 28 | onChangeText: (text: string) => void; 29 | fromLanguage: LanguageCode<'google'>; 30 | toLanguage: LanguageCode<'google'>; 31 | clear: () => void; 32 | reverseLanguage: () => void; 33 | updateFromLanguage: (language: LanguageCode<'google'>) => void; 34 | updateToLanguage: (language: LanguageCode<'google'>) => void; 35 | reverseTranslate: (text: string) => void; 36 | applyClipboard: () => void; 37 | applyHistory: (history: History) => void; 38 | }; 39 | 40 | export const TranslateContext = createContext({} as any); 41 | 42 | const TranslateProvider: React.FC = ({children}) => { 43 | const {addHistory} = useContext(HistoryContext); 44 | const scrollViewRef = useRef(null); // 홈화면 스크롤뷰 45 | const [text, setText] = useState(''); 46 | const [fromLanguage, setFromLanguage] = 47 | useState>('ko'); 48 | const [toLanguage, setToLanguage] = useState>('en'); 49 | const [addHistoryTimer, setAddHistoryTimer] = useState(); 50 | 51 | const clear = useCallback(() => { 52 | // 초기화 53 | setText(''); 54 | }, []); 55 | 56 | const reverseLanguage = useCallback(() => { 57 | // 원문과 번역할 언어를 서로 바꿈 58 | setFromLanguage(toLanguage); 59 | setToLanguage(fromLanguage); 60 | }, [toLanguage, fromLanguage]); 61 | 62 | const reverseTranslate = useCallback( 63 | (_text: string) => { 64 | setText(_text); // text를 적용하고 65 | reverseLanguage(); // 언어도 바꿈 66 | scrollViewRef.current?.scrollTo({y: 0, animated: true}); 67 | // setImmediate(translate); // translate시킴 68 | }, 69 | [reverseLanguage, scrollViewRef], 70 | ); 71 | 72 | const updateFromLanguage = useCallback( 73 | // 원문 언어 변경 74 | (language: LanguageCode<'google'>) => { 75 | if (toLanguage === language) setToLanguage(fromLanguage); // 같은 언어 끼리 번역 방지 76 | setFromLanguage(language); 77 | }, 78 | [toLanguage, fromLanguage], 79 | ); 80 | 81 | const updateToLanguage = useCallback( 82 | // 변역 할 언어 변경 83 | (language: LanguageCode<'google'>) => { 84 | if (fromLanguage === language) setFromLanguage(toLanguage); // 같은 언어 끼리 번역 방지 85 | setToLanguage(language); 86 | if (Math.random() < 0.05) InAppReview.RequestInAppReview(); // 20분의 1 87 | }, 88 | [toLanguage, fromLanguage], 89 | ); 90 | 91 | const applyHistory = useCallback((history: History) => { 92 | // 이전에 검색했던 기록을 다시 검색 93 | setToLanguage(history.toLanguage); 94 | setFromLanguage(history.fromLanguage); 95 | setText(history.text); 96 | scrollViewRef.current?.scrollTo({y: 0, animated: true}); 97 | }, []); 98 | 99 | const applyClipboard = useCallback(async () => { 100 | const content = await Clipboard.getString(); 101 | setText(content); 102 | }, []); 103 | 104 | useEffect(() => { 105 | try { 106 | Tts.setDefaultLanguage(languageTo.ttsLanguage(toLanguage)); 107 | } catch (error) { 108 | Tts.setDefaultLanguage('en-IE'); 109 | } 110 | }, [toLanguage]); 111 | 112 | useEffect(() => { 113 | // 3초동안 새로운 입력이 없으면 최근 검색에 추가 114 | if (addHistoryTimer) clearTimeout(addHistoryTimer); 115 | if (!text) return; 116 | const newTimer = setTimeout( 117 | () => addHistory({fromLanguage, toLanguage, text}), 118 | 3000, 119 | ); 120 | setAddHistoryTimer(newTimer); 121 | }, [text, fromLanguage, toLanguage]); 122 | 123 | const contextValue = useMemo( 124 | () => ({ 125 | scrollViewRef, 126 | text, 127 | onChangeText: t => setText(t), 128 | clear, 129 | fromLanguage, 130 | toLanguage, 131 | reverseLanguage, 132 | reverseTranslate, 133 | updateFromLanguage, 134 | updateToLanguage, 135 | applyHistory, 136 | applyClipboard, 137 | }), 138 | [ 139 | scrollViewRef, 140 | text, 141 | setText, 142 | clear, 143 | fromLanguage, 144 | toLanguage, 145 | reverseLanguage, 146 | reverseTranslate, 147 | updateFromLanguage, 148 | updateToLanguage, 149 | applyHistory, 150 | applyClipboard, 151 | ], 152 | ); 153 | 154 | return ( 155 | 156 | {children} 157 | 158 | ); 159 | }; 160 | 161 | export default TranslateProvider; 162 | -------------------------------------------------------------------------------- /app/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "com.facebook.react" 3 | 4 | import com.android.build.OutputFile 5 | 6 | /** 7 | * This is the configuration block to customize your React Native Android app. 8 | * By default you don't need to apply any configuration, just uncomment the lines you need. 9 | */ 10 | react { 11 | /* Folders */ 12 | // The root of your project, i.e. where "package.json" lives. Default is '..' 13 | // root = file("../") 14 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native 15 | // reactNativeDir = file("../node_modules/react-native") 16 | // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen 17 | // codegenDir = file("../node_modules/react-native-codegen") 18 | // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js 19 | // cliFile = file("../node_modules/react-native/cli.js") 20 | 21 | /* Variants */ 22 | // The list of variants to that are debuggable. For those we're going to 23 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 24 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 25 | // debuggableVariants = ["liteDebug", "prodDebug"] 26 | 27 | /* Bundling */ 28 | // A list containing the node command and its flags. Default is just 'node'. 29 | // nodeExecutableAndArgs = ["node"] 30 | // 31 | // The command to run when bundling. By default is 'bundle' 32 | // bundleCommand = "ram-bundle" 33 | // 34 | // The path to the CLI configuration file. Default is empty. 35 | // bundleConfig = file(../rn-cli.config.js) 36 | // 37 | // The name of the generated asset file containing your JS bundle 38 | // bundleAssetName = "MyApplication.android.bundle" 39 | // 40 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 41 | // entryFile = file("../js/MyApplication.android.js") 42 | // 43 | // A list of extra flags to pass to the 'bundle' commands. 44 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 45 | // extraPackagerArgs = [] 46 | 47 | /* Hermes Commands */ 48 | // The hermes compiler command to run. By default it is 'hermesc' 49 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" 50 | // 51 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 52 | // hermesFlags = ["-O", "-output-source-map"] 53 | } 54 | 55 | /** 56 | * Set this to true to create four separate APKs instead of one, 57 | * one for each native architecture. This is useful if you don't 58 | * use App Bundles (https://developer.android.com/guide/app-bundle/) 59 | * and want to have separate APKs to upload to the Play Store. 60 | */ 61 | def enableSeparateBuildPerCPUArchitecture = false 62 | 63 | /** 64 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode. 65 | */ 66 | def enableProguardInReleaseBuilds = false 67 | 68 | /** 69 | * The preferred build flavor of JavaScriptCore (JSC) 70 | * 71 | * For example, to use the international variant, you can use: 72 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 73 | * 74 | * The international variant includes ICU i18n library and necessary data 75 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 76 | * give correct results when using with locales other than en-US. Note that 77 | * this variant is about 6MiB larger per architecture than default. 78 | */ 79 | def jscFlavor = 'org.webkit:android-jsc:+' 80 | 81 | /** 82 | * Private function to get the list of Native Architectures you want to build. 83 | * This reads the value from reactNativeArchitectures in your gradle.properties 84 | * file and works together with the --active-arch-only flag of react-native run-android. 85 | */ 86 | def reactNativeArchitectures() { 87 | def value = project.getProperties().get("reactNativeArchitectures") 88 | return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] 89 | } 90 | 91 | android { 92 | ndkVersion rootProject.ext.ndkVersion 93 | 94 | compileSdkVersion rootProject.ext.compileSdkVersion 95 | 96 | namespace "com.koreanthinker.translators" 97 | defaultConfig { 98 | applicationId "com.koreanthinker.translators" 99 | minSdkVersion rootProject.ext.minSdkVersion 100 | targetSdkVersion rootProject.ext.targetSdkVersion 101 | versionCode 27 102 | versionName "2.1.5" 103 | } 104 | 105 | splits { 106 | abi { 107 | reset() 108 | enable enableSeparateBuildPerCPUArchitecture 109 | universalApk false // If true, also generate a universal APK 110 | include (*reactNativeArchitectures()) 111 | } 112 | } 113 | signingConfigs { 114 | debug { 115 | storeFile file('debug.keystore') 116 | storePassword 'android' 117 | keyAlias 'androiddebugkey' 118 | keyPassword 'android' 119 | } 120 | release { 121 | if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) { 122 | storeFile file(MYAPP_RELEASE_STORE_FILE) 123 | storePassword MYAPP_RELEASE_STORE_PASSWORD 124 | keyAlias MYAPP_RELEASE_KEY_ALIAS 125 | keyPassword MYAPP_RELEASE_KEY_PASSWORD 126 | } 127 | } 128 | } 129 | buildTypes { 130 | debug { 131 | signingConfig signingConfigs.debug 132 | } 133 | release { 134 | // Caution! In production, you need to generate your own keystore file. 135 | // see https://reactnative.dev/docs/signed-apk-android. 136 | signingConfig signingConfigs.release 137 | minifyEnabled enableProguardInReleaseBuilds 138 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 139 | } 140 | } 141 | 142 | // applicationVariants are e.g. debug, release 143 | applicationVariants.all { variant -> 144 | variant.outputs.each { output -> 145 | // For each separate APK per architecture, set a unique version code as described here: 146 | // https://developer.android.com/studio/build/configure-apk-splits.html 147 | // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. 148 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 149 | def abi = output.getFilter(OutputFile.ABI) 150 | if (abi != null) { // null for the universal-debug, universal-release variants 151 | output.versionCodeOverride = 152 | defaultConfig.versionCode * 1000 + versionCodes.get(abi) 153 | } 154 | 155 | } 156 | } 157 | } 158 | 159 | dependencies { 160 | // The version of react-native is set by the React Native Gradle Plugin 161 | implementation("com.facebook.react:react-android") 162 | 163 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") 164 | 165 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") 166 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { 167 | exclude group:'com.squareup.okhttp3', module:'okhttp' 168 | } 169 | 170 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") 171 | if (hermesEnabled.toBoolean()) { 172 | implementation("com.facebook.react:hermes-android") 173 | } else { 174 | implementation jscFlavor 175 | } 176 | } 177 | 178 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 179 | apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" 180 | -------------------------------------------------------------------------------- /app/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /web/src/context/TranslateContext.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { useContext } from 'react'; 4 | import { Language } from '../constants/types'; 5 | import TranslateProvider, { TranslateContext } from './TranslateContext'; 6 | 7 | describe('TranslateContext', () => { 8 | describe('onChangeText', () => { 9 | it('번역할 문자열을 수정할 수 있습니다.', () => { 10 | const TestComponent = () => { 11 | const { text, onChangeText } = useContext(TranslateContext); 12 | return ( 13 | onChangeText(e.target.value)} 17 | /> 18 | ); 19 | }; 20 | render(, { 21 | wrapper: ({ children }) => ( 22 | {children} 23 | ), 24 | }); 25 | const input = screen.getByRole('input'); 26 | userEvent.type(input, 'test_text'); 27 | expect(input).toHaveValue('test_text'); 28 | }); 29 | it('번역한 문자열을 수정한후 자동으로 번역이 됩니다.', async () => { 30 | const TestComponent = () => { 31 | const { text, onChangeText, translatedData } = 32 | useContext(TranslateContext); 33 | return ( 34 | <> 35 | onChangeText(e.target.value)} 39 | /> 40 | 41 | {translatedData ? JSON.stringify(translatedData) : ''} 42 | 43 | 44 | ); 45 | }; 46 | render(, { 47 | wrapper: ({ children }) => ( 48 | {children} 49 | ), 50 | }); 51 | 52 | // 임의의 번역할 값 넣기 53 | const input = screen.getByRole('input'); 54 | userEvent.type(input, 'test_text'); 55 | // output이 생길때까지 기다림 56 | await waitFor(() => 57 | expect(!!screen.getByRole('output').innerHTML).toBeTruthy(), 58 | ); 59 | // output이 유요한지 확인 60 | const output = screen.getByRole('output'); 61 | const translated = JSON.parse(output.innerHTML); 62 | expect(translated).toHaveProperty('google'); 63 | expect(translated).toHaveProperty('papago'); 64 | expect(translated).toHaveProperty('kakao'); 65 | }); 66 | }); 67 | describe('reverseTranslate', () => { 68 | it('번역된 문자열을 언어를 서로바꿔 다시 번역합니다.', async () => { 69 | const TestComponent = () => { 70 | const { 71 | text, 72 | onChangeText, 73 | translatedData, 74 | reverseTranslate, 75 | fromLanguage, 76 | toLanguage, 77 | } = useContext(TranslateContext); 78 | return ( 79 | <> 80 | onChangeText(e.target.value)} 84 | /> 85 | 86 | {translatedData ? JSON.stringify(translatedData) : ''} 87 | 88 | {fromLanguage} 89 | {toLanguage} 90 | 94 | 95 | ); 96 | }; 97 | render(, { 98 | wrapper: ({ children }) => ( 99 | {children} 100 | ), 101 | }); 102 | 103 | const prevFromLanguage = screen.getByRole('fromLanguage').innerHTML; 104 | const prevToLanguage = screen.getByRole('toLanguage').innerHTML; 105 | // 임의의 번역할 값 넣기 106 | const input = screen.getByRole('input'); 107 | userEvent.type(input, 'test_text'); 108 | // output이 생길때까지 기다림 109 | await waitFor(() => 110 | expect(!!screen.getByRole('output').innerHTML).toBeTruthy(), 111 | ); 112 | // output이 유요한지 확인 113 | const output = screen.getByRole('output'); 114 | const translated = JSON.parse(output.innerHTML); 115 | expect(translated).toHaveProperty('google'); 116 | expect(translated).toHaveProperty('papago'); 117 | expect(translated).toHaveProperty('kakao'); 118 | const prevGoogleTranslated = translated.google; 119 | // reverseTranslate 실행 120 | userEvent.click(screen.getByRole('reverseTranslate')); 121 | // 언어 변경 확인 122 | const newFromLanguage = screen.getByRole('fromLanguage').innerHTML; 123 | const newToLanguage = screen.getByRole('toLanguage').innerHTML; 124 | expect(prevFromLanguage).toBe(newToLanguage); 125 | expect(prevToLanguage).toBe(newFromLanguage); 126 | // 기존의 번역결과가 원문으로 적용됬는지 확인 127 | expect(screen.getByRole('input')).toHaveValue(prevGoogleTranslated); 128 | }); 129 | }); 130 | 131 | describe('updateFromLanguage', () => { 132 | it('원문의 언어를 수정할 수 있습니다.', () => { 133 | const toBeUpdateValue: Language = 'fr'; 134 | const TestComponent = () => { 135 | const { updateFromLanguage, fromLanguage } = 136 | useContext(TranslateContext); 137 | return ( 138 | <> 139 | {fromLanguage} 140 |