More languages

Seto Elkahfi committed Jan 23, 2022 at 16:08 UTC 5c2e1d99f373867bbbdb197b3fb3f0e23fb0e3c6
16 files changed +282 -63
src/components/App.tsx
+21 -6
@@ -5,14 +5,22 @@ import FooterLinks from './FooterLinks';
5 import { IntlProvider, addLocaleData } from 'react-intl';
6 import { BrowserRouter } from 'react-router-dom';
7 import Firebase, { FirebaseContext } from '../components/Firebase';
8 -import locale_en from 'react-intl/locale-data/en';
9 -import locale_se from 'react-intl/locale-data/se';
10 -import locale_id from 'react-intl/locale-data/id';
11 -import messages_se from "../translations/se.json";
8 +import localeEn from 'react-intl/locale-data/en';
9 +import localeSe from 'react-intl/locale-data/se';
10 +import localeId from 'react-intl/locale-data/id';
11 +import localeDe from 'react-intl/locale-data/de';
12 +import localeFr from 'react-intl/locale-data/fr';
13 +import localeZh from 'react-intl/locale-data/zh';
14 +import localeEs from 'react-intl/locale-data/es';
15 +import messages_sv from "../translations/sv.json";
16 import messages_id from "../translations/id.json";
17 import messages_en from "../translations/en.json";
18 +import messagesDe from "../translations/de.json";
19 +import messagesFr from "../translations/fr.json";
20 +import messagesZh from "../translations/zh.json";
21 +import messagesEs from "../translations/es.json";
22
15 -addLocaleData([...locale_en, ...locale_id, ...locale_se]);
23 +addLocaleData([...localeEn, ...localeId, ...localeSe, ...localeDe, ...localeFr, ...localeZh, ...localeEs]);
24
25 let i18nConfig = {
26 language: 'en',
@@ -31,13 +39,20 @@ class App extends Component<AppProps, AppState> {
39
40 onChangeLanguage(language: string) {
41 switch (language) {
34 - case 'se': i18nConfig.messages = messages_se; break;
42 + case 'se': i18nConfig.messages = messages_sv; break;
43 case 'en': i18nConfig.messages = messages_en; break;
44 case 'id': i18nConfig.messages = messages_id; break;
45 + case 'de': i18nConfig.messages = messagesDe; break;
46 + case 'fr': i18nConfig.messages = messagesFr; break;
47 + case 'zh': i18nConfig.messages = messagesZh; break;
48 + case 'es': i18nConfig.messages = messagesEs; break;
49 default: i18nConfig.messages = messages_en; break;
50 }
51 i18nConfig.language = language;
52 this.setState({ language: language })
53 + if (this.firebase) {
54 + this.firebase.setLanguage(language)
55 + }
56 }
57
58 createFirebaseContextIfNeeded(language: string): Firebase {
src/components/AudioPlayer.tsx
+82 -30
@@ -3,13 +3,13 @@
3 import axios from 'axios';
4 import React, { CSSProperties } from 'react';
5 import { Component } from 'react';
6 -import { Col, Row, Spinner, ToggleButtonGroup } from 'react-bootstrap';
6 +import { Col, Row, Spinner } from 'react-bootstrap';
7 import YouTube, { Options } from 'react-youtube';
8 import { GiMicrophone, GiDrumKit, GiGuitarHead, GiGuitarBassHead, GiPianoKeys, GiPlayButton, GiPauseButton } from 'react-icons/gi';
9 import { IconContext } from 'react-icons';
10 import { db, AudioFiles } from './db';
11 -import { Channel, Player, Time, Transport } from 'tone';
12 -import { threadId } from 'worker_threads';
11 +import { FormattedMessage } from 'react-intl';
12 +import { Channel, Player} from 'tone';
13
14 const buttonStyle: CSSProperties = {
15
@@ -25,6 +25,12 @@ interface AudioFile {
25 results: Result[]
26 }
27
28 +interface YouTubeEventTarget {
29 + pauseVideo: () => void,
30 + playVideo: () => void,
31 + seekTo: (arg0: number) => void
32 +}
33 +
34 type AudioProps = {
35 audioFileId: string
36 }
@@ -54,6 +60,8 @@ class AudioPlayer extends Component<AudioProps, AudioState> {
60 drumsPlayer: Player | null = null;
61 pianoPlayer: Player | null = null;
62
63 + youTubeEventTarget: YouTubeEventTarget | null = null
64 +
65 constructor(props: AudioProps) {
66 super(props);
67 this.state = {
@@ -61,7 +69,7 @@ class AudioPlayer extends Component<AudioProps, AudioState> {
69 playerTime: 0,
70 isPlayerReady: false,
71 isYoutubePlayerReady: false,
64 - isPlaying: true,
72 + isPlaying: false,
73 vocalAudioOn: true,
74 guitarAudioOn: true,
75 bassAudioOn: true,
@@ -82,7 +90,12 @@ class AudioPlayer extends Component<AudioProps, AudioState> {
90 }
91
92 componentWillUnmount() {
85 - this.togglePlayAudio();
93 + this.setState({isPlaying: false})
94 + this.vocalPlayer?.stop()
95 + this.bassPlayer?.stop()
96 + this.drumsPlayer?.stop()
97 + this.pianoPlayer?.stop()
98 + this.otherPlayer?.stop()
99 }
100
101 downloadAudioFilesIfNeeded() {
@@ -195,17 +208,20 @@ class AudioPlayer extends Component<AudioProps, AudioState> {
208 togglePlayAudio() {
209 this.setState({isPlaying: !this.state.isPlaying})
210 if (this.state.isPlaying) {
198 - this.vocalPlayer?.start()
199 - this.bassPlayer?.start()
200 - this.drumsPlayer?.start()
201 - this.pianoPlayer?.start()
202 - this.otherPlayer?.start()
211 + this.vocalPlayer?.stop()
212 + this.bassPlayer?.stop()
213 + this.drumsPlayer?.stop()
214 + this.pianoPlayer?.stop()
215 + this.otherPlayer?.stop()
216 + this.youTubeEventTarget?.pauseVideo()
217 } else {
204 - this.vocalPlayer?.stop();
205 - this.bassPlayer?.stop();
206 - this.drumsPlayer?.stop();
207 - this.pianoPlayer?.stop();
208 - this.otherPlayer?.stop();
218 + this.youTubeEventTarget?.seekTo(0)
219 + this.youTubeEventTarget?.playVideo()
220 + this.vocalPlayer?.start();
221 + this.bassPlayer?.start();
222 + this.drumsPlayer?.start();
223 + this.pianoPlayer?.start();
224 + this.otherPlayer?.start();
225 }
226 }
227
@@ -252,25 +268,47 @@ class AudioPlayer extends Component<AudioProps, AudioState> {
268 showinfo: 0
269 },
270 };
255 -
256 - let togglePlayButton = <GiPauseButton
257 - color={this.state.isPlaying ? `white` : `red`}
258 - onClick={() => this.togglePlayAudio() }
259 - style={buttonStyle} />
260 - if (this.state.isPlaying) {
261 - togglePlayButton = <GiPlayButton
262 - color={this.state.isPlaying ? `white` : `red`}
263 - onClick={() => this.togglePlayAudio() }
264 - style={buttonStyle} />
271 + let togglePlayButton = <p>Waiting for YouTube...</p>
272 + if (this.state.isYoutubePlayerReady) {
273 + let button
274 + if (this.state.isPlaying) {
275 + button = <Col>
276 + <GiPauseButton
277 + color={this.state.isPlaying ? `white` : `red`}
278 + onClick={() => this.togglePlayAudio() }
279 + style={buttonStyle} />
280 + </Col>
281 + } else {
282 + button = <Col>
283 + <GiPlayButton
284 + color={this.state.isPlaying ? `white` : `red`}
285 + onClick={() => this.togglePlayAudio() }
286 + style={buttonStyle} />
287 + </Col>
288 + }
289 + togglePlayButton = <Col>
290 + <p>
291 + <FormattedMessage id="player.instruction"
292 + defaultMessage="Click isntrument icon to mute part of the song."
293 + description="Instruction message"/>
294 + </p>
295 + {button}
296 + </Col>
297 }
298
299 return (
300 <IconContext.Provider value={{ size: "2em", color: "white", className: "global-class-name" }}>
301 <Row className="mb-3 mt-3">
302 <Col>
271 - {togglePlayButton}
303 + <h1>SplitFire AI</h1>
304 + <a href="https://testflight.apple.com/join/4cb0rDIo" target={`__blank`}>
305 + <img src='https://splitfire.ai/packs/media/images/Pre-order_on_the_App_Store_Badge_US-UK_RGB_blk_121217-5fb138d3f649c68f7b3250e3887dbef5.svg' />
306 + </a>
307 </Col>
308 </Row>
309 + <Row className="mb-3 mt-3">
310 + {togglePlayButton}
311 + </Row>
312 <Row className="mb-3 mt-3">
313 <Col>
314 <GiMicrophone
@@ -304,7 +342,8 @@ class AudioPlayer extends Component<AudioProps, AudioState> {
342 </Col>
343 </Row>
344 <Row>
307 - <Col>
345 + <Col style={{display: this.state.isYoutubePlayerReady ? 'block' : 'none'}}>
346 +
347 <YouTube
348 videoId={this.state.audioFile?.youtube_video_id}
349 opts={opts}
@@ -318,8 +357,16 @@ class AudioPlayer extends Component<AudioProps, AudioState> {
357 } else {
358 return (
359 <Row>
360 + <Row className="mb-3 mt-3">
361 + <Col>
362 + <h1>SplitFire AI</h1>
363 + <a href="https://testflight.apple.com/join/4cb0rDIo" target={`__blank`}>
364 + <img src='https://splitfire.ai/packs/media/images/Pre-order_on_the_App_Store_Badge_US-UK_RGB_blk_121217-5fb138d3f649c68f7b3250e3887dbef5.svg' />
365 + </a>
366 + </Col>
367 + </Row>
368 <Col className="mb-3 mt-3">
322 - <p>Loading player...</p>
369 + <p>🤖 AI is doing its business...</p>
370 <Spinner animation='grow'></Spinner>
371 </Col>
372 </Row>
@@ -328,14 +375,19 @@ class AudioPlayer extends Component<AudioProps, AudioState> {
375 }
376
377 _onReady(event: any) {
331 - console.log('YouTube ready: ', event);
332 - this.setState({ isYoutubePlayerReady: true });
378 + console.log('YouTube ready: ', event)
379 + this.youTubeEventTarget = event.target
380 + this.youTubeEventTarget?.playVideo()
381 + setTimeout(() => {this.youTubeEventTarget?.pauseVideo()}, 5000)
382 }
383
384 _onStateChange(event: any) {
385 console.log('State changed: ', event)
386 console.log('getCurrentTime: ', event.target.getCurrentTime())
387 console.log('getPlayerState: ', event.target.getPlayerState())
388 + if (!this.state.isYoutubePlayerReady && event.target.getCurrentTime() > 1) {
389 + this.setState({ isYoutubePlayerReady: true })
390 + }
391 }
392 }
393
src/components/Firebase/firebase.ts
+5
@@ -16,6 +16,8 @@ class Firebase {
16 db: app.database.Database;
17 language: string;
18
19 + public onLanguageChangedCallback: (() => void) | null = null
20 +
21 constructor(language: string) {
22 app.initializeApp(config);
23 this.db = app.database();
@@ -24,6 +26,9 @@ class Firebase {
26
27 setLanguage(language: string) {
28 this.language = language
29 +
30 + if (this.onLanguageChangedCallback)
31 + this.onLanguageChangedCallback()
32 }
33
34 rootRef(): app.database.Reference {
src/components/FooterLinks.tsx
+5 -1
@@ -24,7 +24,11 @@ const FooterLinks = () => (
24 <footer className="text-muted py-5">
25 <div className="container">
26 <p className="float-end mb-1">
27 - <a href="#" style={linkStyle}>Back to top</a>
27 + <a href="#" style={linkStyle}>
28 + <FormattedMessage id="footer.to_top"
29 + defaultMessage="Back to top"
30 + description="To top link" />
31 + </a>
32 </p>
33 <p className="mb-0">
34 <Link to='/about' style={linkStyle}>
src/components/Header.tsx
+14 -2
@@ -1,6 +1,7 @@
1 import React, { Component } from 'react';
2 import { FormattedMessage } from 'react-intl';
3 import { Link } from 'react-router-dom';
4 +import LanguageDropdown from './LanguageDropdown';
5
6 type HeaderProps = {
7 onChangeLanguage: (language: string) => void
@@ -33,16 +34,26 @@ class Header extends Component<HeaderProps, HeaderState> {
34 defaultMessage="About"
35 description="About page title" /></h4>
36 <p className="text-muted">We buy things we don't need, with money we don't have, to impress people we don't like.</p>
37 + <p className="text-muted">Get your first <a href="https://www.americanexpress.se/bjudin/setoE9El2?CPID=999999550" target="_blank">American Express card</a>.</p>
38 + <p className="text-muted">Get your proper vm, <a href="https://hetzner.cloud/?ref=WwAE3O6U53DF">Kamerad</a>.</p>
39 </div>
40 <div className="col-sm-2 offset-md-1 py-4">
38 - <h4 className="text-white">Fun projects</h4>
41 + <h4 className="text-white">
42 + <FormattedMessage id="header.fun"
43 + defaultMessage="Fun projects"
44 + description="Fun projects title"/></h4>
45 <ul className="list-unstyled">
46 <li><a className="text-white" href="https://musik88.com/" title="Musik88" target="_blank" rel="noopener noreferrer">Musik88</a></li>
47 <li><a className="text-white" href="https://splitfire.ai/" title="SplitFire AI" target="_blank" rel="noopener noreferrer">SplitFire AI</a></li>
48 + <li><a className="text-white" href="https://smbpndk.com/" title="SumbuPendek" target="_blank" rel="noopener noreferrer">SumbuPendek</a></li>
49 + <li><a className="text-white" href="https://scandinasia.eu/" title="SumbuPendek" target="_blank" rel="noopener noreferrer">United States of Scandinasia</a></li>
50 </ul>
51 </div>
52 <div className="col-sm-2 py-4">
45 - <h4 className="text-white">Contact</h4>
53 + <h4 className="text-white">
54 + <FormattedMessage id="contact.title"
55 + defaultMessage="Contact"
56 + description="Contact page title"/></h4>
57 <ul className="list-unstyled">
58 <li><a className="text-white" href="https://stackoverflow.com/users/1137814/seto" title="StackOverflow" target="_blank" rel="noopener noreferrer">StackOverflow</a></li>
59 <li><a className="text-white" href="https://github.com/setoelkahfi" title="GitHub" target="_blank" rel="noopener noreferrer">GitHub</a></li>
@@ -58,6 +69,7 @@ class Header extends Component<HeaderProps, HeaderState> {
69 <Link to='/' className="navbar-brand d-flex align-items-center">
70 <strong>@seto</strong>
71 </Link>
72 + <LanguageDropdown onChangeLanguage={this.props.onChangeLanguage} />
73 <button className="navbar-toggler" onClick={this.toggleExpand.bind(this)} type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded={this.state.expand} aria-label="Toggle navigation">
74 <span className="navbar-toggler-icon"></span>
75 </button>
src/components/Home.tsx
+30 -15
@@ -26,7 +26,10 @@ const wordStyle = {
26 const initialValues: any = {
27 "en": ["an Android developer", "an iOS developer"],
28 "id": ["pengembang aplikasi Android", "pengembang aplikasi iOS"],
29 - "se": ["är Android-utvecklare", "är iOS-utvecklare"]
29 + "sv": ["är Android-utvecklare", "är iOS-utvecklare"],
30 + "es": ["an Android developer", "an iOS developer"],
31 + "de": ["an Android developer", "an iOS developer"],
32 + "fr": ["an Android developer", "an iOS developer"],
33 };
34
35
@@ -43,7 +46,11 @@ type HomeState = {
46
47 class Home extends Component<HomeProps, HomeState> {
48
46 - whoAmIRef: app.database.Reference | undefined;
49 + whoAmIRef: app.database.Reference | undefined
50 +
51 + onLanguageChangedCallback = () => {
52 + console.log("onLanguageChangedCallback")
53 + }
54
55 constructor(props: HomeProps) {
56 super(props);
@@ -54,20 +61,12 @@ class Home extends Component<HomeProps, HomeState> {
61 alterEgos: initialValues[this.props.firebase?.language ?? 'en'],
62 files: []
63 }
64 + if (this.props.firebase)
65 + this.props.firebase.onLanguageChangedCallback = this.onLanguageChangedCallback.bind(this)
66 }
67
68 componentDidMount() {
60 - this.whoAmIRef?.on('value', (snapshot) => {
61 - let items = snapshot?.val();
62 - let whoAmI = [];
63 - for (let item in items) {
64 - whoAmI.push(items[item].content);
65 - }
66 - this.setState({
67 - alterEgos: this.shuffle(whoAmI)
68 - });
69 - });
70 -
69 + this._updateTranslationIfNeeded()
70 axios.get(`/splitfire`)
71 .then(res => {
72 const files = res.data.audio_files;
@@ -77,6 +76,21 @@ class Home extends Component<HomeProps, HomeState> {
76
77 componentWillUnmount() {
78 this.whoAmIRef?.off();
79 + if (this.props.firebase)
80 + this.props.firebase.onLanguageChangedCallback = null
81 + }
82 +
83 + _updateTranslationIfNeeded() {
84 + this.whoAmIRef?.on('value', (snapshot) => {
85 + let items = snapshot?.val();
86 + let whoAmI = [];
87 + for (let item in items) {
88 + whoAmI.push(items[item].content);
89 + }
90 + this.setState({
91 + alterEgos: this.shuffle(whoAmI)
92 + });
93 + })
94 }
95
96 shuffle(array: any[]) {
@@ -99,9 +113,10 @@ class Home extends Component<HomeProps, HomeState> {
113 }
114
115 render() {
102 - const { files } = this.state;
116 + const { files } = this.state
117 +
118 + let caraoselContent: any = 'Loading SplitFire AI...'
119
104 - let caraoselContent: any = 'Loading SplitFire AI...';
120 if (files.length > 0) {
121 caraoselContent = <Carousel>
122 {files.slice(0, 3).map(item => (
src/components/LanguageDropdown.tsx
+46 -3
@@ -33,7 +33,7 @@ class LanguageDropdown extends Component<LanguageDropdownProps, LanguageDropdown
33 name: "🇺🇸 English"
34 }
35
36 - const sv = {
36 + const se = {
37 value: "se",
38 name: "🇸🇪 Swedish"
39 }
@@ -43,12 +43,55 @@ class LanguageDropdown extends Component<LanguageDropdownProps, LanguageDropdown
43 name: "🇮🇩 Indonesian"
44 }
45
46 + const fr = {
47 + value: "fr",
48 + name: "🇫🇷 Français"
49 + }
50 +
51 + const de = {
52 + value: "de",
53 + name: "🇩🇪 Deutsch"
54 + }
55 +
56 + const zh = {
57 + value: "zh",
58 + name: "🇨🇳 中国人"
59 + }
60 +
61 + const es = {
62 + value: "es",
63 + name: "🇪🇸 Español"
64 + }
65 +
66 var languageList: OptionProps[] = []
67 languageList.push(id)
48 - languageList.push(sv)
68 + languageList.push(se)
69 languageList.push(en)
70 + languageList.push(de)
71 + languageList.push(fr)
72 + languageList.push(zh)
73 + languageList.push(es)
74 +
75 + return this.shuffle(languageList)
76 + }
77 +
78 + shuffle(array: any[]) {
79 + var currentIndex = array.length, temporaryValue, randomIndex;
80 +
81 + // While there remain elements to shuffle...
82 + while (0 !== currentIndex) {
83 +
84 + // Pick a remaining element...
85 + randomIndex = Math.floor(Math.random() * currentIndex);
86 + currentIndex -= 1;
87 +
88 + // And swap it with the current element.
89 + temporaryValue = array[currentIndex];
90 + array[currentIndex] = array[randomIndex];
91 + array[randomIndex] = temporaryValue;
92 + }
93
51 - return languageList
94 + return array;
95 }
96
97 render() {
src/components/Main.tsx
+1 -1
@@ -18,7 +18,7 @@ const mainStyle = {
18
19 const contentStyle = {
20 borderRadius: '100px!important',
21 - backgroundColor: 'rgba(52, 52, 52, 0.5)'
21 + backgroundColor: 'rgba(52, 52, 52, 0.6)'
22 }
23
24 const Main = () => (
src/translations/de.json new
+15
@@ -0,0 +1,15 @@
1 +{
2 + "header.home": "Heim",
3 + "header.about": "Etwa",
4 + "header.cv": "CV",
5 + "header.contact": "Kontakt",
6 + "header.fun": "Lustige Projekte",
7 + "home.title": "Hallo, mein Name ist {name}.",
8 + "home.iam": "Ich bin ",
9 + "about.title": "Etwa",
10 + "about.skills": "Fähigkeiten",
11 + "cv.title": "CV",
12 + "contact.title": "Kontakt",
13 + "player.instruction": "Klicken Sie auf das Instrumentensymbol, um es aus dem Song zu entfernen.",
14 + "footer.to_top": "Zurück nach oben"
15 +}
src/translations/en.json
+4 -1
@@ -3,10 +3,13 @@
3 "header.about": "About",
4 "header.cv": "CV",
5 "header.contact": "Contact",
6 + "header.fun": "Fun projects",
7 "home.title": "Hello, my name is {name}.",
8 "home.iam": "I'm ",
9 "about.title": "About",
10 "about.skills": "Skills",
11 "cv.title": "CV",
11 - "contact.title": "Contact"
12 + "contact.title": "Contact",
13 + "player.instruction": "Click the instrument icon to remove it from the song.",
14 + "footer.to_top": "Back to top"
15 }
src/translations/es.json new
+15
@@ -0,0 +1,15 @@
1 +{
2 + "header.home": "Casa",
3 + "header.about": "Sobre",
4 + "header.cv": "CV",
5 + "header.contact": "Contacto",
6 + "header.fun": "Proyectos divertidos",
7 + "home.title": "Hola, mi nombre es {name}.",
8 + "home.iam": "Estoy ",
9 + "about.title": "Acerca de",
10 + "about.skills": "Habilidades",
11 + "cv.title": "CV",
12 + "contact.title": "Contacto",
13 + "player.instruction": "Haz clic en el ícono del instrumento para eliminarlo de la canción.",
14 + "footer.to_top": "Volver arriba"
15 +}
src/translations/fr.json new
+15
@@ -0,0 +1,15 @@
1 +{
2 + "header.home": "Accueil",
3 + "header.about": "Sur",
4 + "header.cv": "CV",
5 + "header.contact": "Contact",
6 + "header.fun": "Projets amusants",
7 + "home.title": "Bonjour, je m'appelle {name}.",
8 + "home.iam": "Je suis ",
9 + "about.title": "Sur",
10 + "about.skills": "Compétences",
11 + "cv.title": "CV",
12 + "contact.title": "Contact",
13 + "player.instruction": "Cliquez sur l'icône de l'instrument pour le supprimer du morceau.",
14 + "footer.to_top": "Retour en haut"
15 +}
src/translations/id.json
+4 -1
@@ -3,10 +3,13 @@
3 "header.about": "Tentang",
4 "header.cv": "CV",
5 "header.contact": "Kontak",
6 + "header.fun": "Iseng-iseng berhadiah",
7 "home.title": "Halo, nama saya {name}.",
8 "home.iam": "Saya ",
9 "about.title": "Tentang",
10 "about.skills": "Keterampilan",
11 "cv.title": "CV",
11 - "contact.title": "Kontak"
12 + "contact.title": "Kontak",
13 + "player.instruction": "Klik ikon instrumen untuk menghapusnya dari lagu.",
14 + "footer.to_top": "Kembali ke atas"
15 }
src/translations/sv.json renamed
+5 -2
@@ -3,10 +3,13 @@
3 "header.about": "Om",
4 "header.cv": "CV",
5 "header.contact": "Kontakt",
6 + "header.fun": "Roliga projekt",
7 "home.title": "Tjena, jag heter {name}.",
8 "home.iam": "Jag ",
9 "about.title": "Om",
10 "about.skills": "Kompetens",
11 "cv.title": "CV",
11 - "contact.title": "Kontakt"
12 - }
12 + "contact.title": "Kontakt",
13 + "player.instruction": "Klicka på instrumentikonen för att ta bort den från låten.",
14 + "footer.to_top": "Tillbaka till toppen"
15 + }
\ No newline at end of file
src/translations/zh.json new
+15
@@ -0,0 +1,15 @@
1 +{
2 + "header.home": "家",
3 + "header.about": "关于",
4 + "header.cv": "CV",
5 + "header.contact": "接触",
6 + "header.fun": "有趣的项目",
7 + "home.title": "你好我的名字是 {name}.",
8 + "home.iam": "我是",
9 + "about.title": "关于",
10 + "about.skills": "技能",
11 + "cv.title": "CV",
12 + "contact.title": "接触",
13 + "player.instruction": "点击乐器图标将其从歌曲中移除。",
14 + "footer.to_top": "回到顶部"
15 +}
types/react-intl/index.d.ts
+5 -1
@@ -1,4 +1,8 @@
1 declare module 'react-intl';
2 declare module 'react-intl/locale-data/se';
3 declare module 'react-intl/locale-data/en';
4 -declare module 'react-intl/locale-data/id';
\ No newline at end of file
4 +declare module 'react-intl/locale-data/id';
5 +declare module 'react-intl/locale-data/de';
6 +declare module 'react-intl/locale-data/fr';
7 +declare module 'react-intl/locale-data/zh';
8 +declare module 'react-intl/locale-data/es';
\ No newline at end of file