Skip to content

Commit 967387d

Browse files
committed
feat: add i18n support
1 parent ca215cb commit 967387d

File tree

120 files changed

+300
-97
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+300
-97
lines changed

crawler_new.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {sort} from "fast-sort";
44

55
let work = ['2024_4', '2025_1']
66

7-
if(false) {
7+
if (false) {
88
work = []
99
for(let year = 2012;year<=2024;year++) {
1010
for(let term = 1;term<=4;term++) {
@@ -31,12 +31,13 @@ function parseTime(times) {
3131
}).filter(x => x)
3232
}
3333

34-
async function main(year = 2024, term = 4, file = `static/result_${year}_${term}.json`) {
34+
async function main(year = 2024, term = 4, lang = 'ko', file = `static/${lang}/result_${year}_${term}.json`) {
3535
const raw = Object.values(await fetch('https://erp.kaist.ac.kr/sch/sles/SlesseCtr/findAllEstblSubjtList.do', {
3636
body: `_menuId=MTI0ODU1MjEwNTgwMTA3ODAwMDA%3D&_menuNm=%EC%A0%84%EC%B2%B4%EA%B0%9C%EC%84%A4%EA%B5%90%EA%B3%BC%EB%AA%A9%EC%A1%B0%ED%9A%8C&_pgmId=NzU0MzkyMTUwMjY%3D&%40d1%23syy=${year}&%40d1%23smtDivCd=${term}&%40d1%23deptCd=806&%40d1%23lwprtInclsYn=1&%40d1%23subjtCrseDivCd=&%40d1%23subjcDivCd=%2C13%2C32%2C12%2C75%2C30%2C31%2C76%2C73%2C74%2C10%2C70%2C71%2C11%2C77%2C34%2C72%2C35%2C36%2C37%2C38%2C39%2C40%2C41%2C42%2C43%2C33%2C78%2C96%2C95%2C93%2C94%2C92%2C90%2C91%2CZM%2CZN%2CZQ%2CZR%2CZW%2CZJ%2CZV%2CZZ%2CZL%2CZS%2CZY%2CZX%2CZO%2CZP%2CZT%2CZU%2CZK&%40d1%23subjtNm=&%40d1%23fromCdt=&%40d1%23toCdt=&%40d1%23englSubjtYn=&%40d1%23subjtCd=&%40d1%23chrgNm=&%40d1%23fromAtnlcPercpCnt=&%40d1%23toAtnlcPercpCnt=&%40d%23=%40d1%23&%40d1%23=dmCond&%40d1%23tp=dm&`,
3737
method: 'POST',
3838
headers: {
39-
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
39+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
40+
'Cookie': `locale=${lang}`
4041
}
4142
}).then(r => r.json()))[0]
4243

@@ -68,7 +69,7 @@ async function main(year = 2024, term = 4, file = `static/result_${year}_${term}
6869
} catch (e) {
6970
}
7071

71-
const deptMap = {"833": "수리과학과"};
72+
const deptMap = {"833": {ko: '수리과학과', en: 'Department of Mathematical Sciences'}[lang]};
7273
raw.forEach(r => deptMap[r.deprtCd] = r.deprtNm)
7374

7475
const res = {
@@ -78,15 +79,15 @@ async function main(year = 2024, term = 4, file = `static/result_${year}_${term}
7879
writeFileSync(file, JSON.stringify(res))
7980
}
8081

81-
function other() {
82-
const fileNames = fs.readdirSync('static')
82+
function other(lang = 'ko') {
83+
const fileNames = fs.readdirSync(`static/${lang}`)
8384
const files = fileNames.filter(name => name.startsWith('result_')).map(name => {
8485
const li = name.split('_')
8586
return {year: +li[1], term: +li[2].split('.')[0], name}
8687
})
8788
const subjects = {}, titles = {}
8889
files.forEach(({year, term, name}) => {
89-
const data = fs.readFileSync(`static/${name}`)
90+
const data = fs.readFileSync(`static/${lang}/${name}`)
9091
const json = JSON.parse(data)
9192
json.data.forEach(subject => {
9293
if (subject.code.includes('URP')) return
@@ -125,13 +126,13 @@ function other() {
125126
}
126127
subjects[code][1] = sort(li).desc(['0'])
127128
}
128-
writeFileSync('static/other.json', JSON.stringify(subjects))
129+
writeFileSync(`static/${lang}/other.json`, JSON.stringify(subjects))
129130
}
130131

131132
Promise.all(work.map(w => {
132133
const li = w.split('_')
133-
return main(+li[0], +li[1], `static/result_${w}.json`)
134-
})).then(() => {
134+
return [main(+li[0], +li[1], 'ko'), main(+li[0], +li[1], 'en')]
135+
}).flat()).then(() => {
135136
other()
136137
console.log('done')
137138
})

src/lib/LectureList.svelte

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import {sort} from "fast-sort";
77
import Scrolling from "$lib/Scrolling.svelte";
88
import Other from "$lib/Other.svelte";
9+
import {locale, str} from "$lib";
910
1011
export let list = [], favorites = [], deptMap = {}, hover, selected, mobile, year, term, timeSegments, selTime,
1112
detail, hideTerm, compete;
1213
13-
const types = [
14+
$: types = $locale === 'ko' ? [
1415
"기초필수",
1516
"기초선택",
1617
"전공필수",
@@ -19,7 +20,16 @@
1920
"인문사회선택",
2021
"공통필수",
2122
"자유선택",
22-
];
23+
] : [
24+
"Basic Required",
25+
"Basic Elective",
26+
"Major Required",
27+
"Major Elective",
28+
"Mandatory General Courses",
29+
"Humanities & Social Elective",
30+
"General Required",
31+
"Other Elective",
32+
]
2333
let page = 1, search = '', dept, deptList = [], type = null;
2434
2535
$: _list = sort(list.map(i => ({
@@ -64,45 +74,48 @@
6474
6575
let way = 0
6676
$: if (way >= 4) way = 0
77+
78+
79+
$: l = str[$locale]
6780
</script>
6881

6982
<div style="position: sticky;top: 0px;background:var(--surface);z-index: 10;padding-top: 12px">
7083
<header>
7184
{#if year && !hideTerm}
7285
<div class="item">
73-
<Select bind:selected={year} placeholder="년도" {mobile}>
86+
<Select bind:selected={year} placeholder={l['YEAR']} {mobile}>
7487
{#each Array.from({length: 12}, (_, i) => 2024 - i) as y}
7588
<Option title={y} data={y}/>
7689
{/each}
7790
</Select>
7891
</div>
7992
<div class="item">
80-
<Select bind:selected={term} placeholder="학기" {mobile}>
81-
<Option title="봄" data={1}/>
82-
<Option title="여름" data={2}/>
83-
<Option title="가을" data={3}/>
84-
<Option title="겨울" data={4}/>
93+
<Select bind:selected={term} placeholder={l['TERM']} {mobile}>
94+
<Option title={l['SPRING']} data={1}/>
95+
<Option title={l['SUMMER']} data={2}/>
96+
<Option title={l['FALL']} data={3}/>
97+
<Option title={l['WINTER']} data={4}/>
8598
</Select>
8699
</div>
87100
{/if}
88101
<div style="flex: 1;min-width: 160px">
89-
<Input bind:value={search} placeholder="검색" fullWidth/>
102+
<Input bind:value={search} placeholder={l['SEARCH']} fullWidth/>
90103
</div>
91104
<div class="item" style="flex: 1">
92-
<Select bind:selected={dept} placeholder="학과" {mobile} fullWidth>
105+
<Select bind:selected={dept} placeholder={l['DEPT']} {mobile} fullWidth>
93106
<main on:wheel|stopPropagation|passive on:touchmove|stopPropagation|passive
94107
on:touchdown|stopPropagation|passive style="max-height: 80vh">
95-
<Option title="전체" data={null}/>
108+
<Option title={l['ALL']} data={null}/>
96109
{#each deptList as dept}
97110
<Option title={deptMap[dept]} data={dept}/>
98111
{/each}
99112
</main>
100113
</Select>
101114
</div>
102115
<div class="item" style="flex: 0.6">
103-
<Select bind:selected={type} placeholder="유형" {mobile} style="max-height: 80vh;min-width: 100px" fullWidth>
116+
<Select bind:selected={type} placeholder={l['TYPE']} {mobile} style="max-height: 80vh;min-width: 100px" fullWidth>
104117
<main on:wheel|stopPropagation|passive>
105-
<Option title="전체" data={null}/>
118+
<Option title={l['ALL']} data={null}/>
106119
{#each types as type}
107120
<Option title={type} data={type}/>
108121
{/each}
@@ -121,7 +134,7 @@
121134
<div style="margin: -8px 0 6px 0;display: flex;align-items: center">
122135
<Paper left xstack bottom>
123136
<Button small outlined icon="timer" slot="target"
124-
round>{!selTime ? '전체' : ['', '', '', '', ''][+selTime[0]] + tForm}</Button>
137+
round>{!selTime ? l['ALL'] : [l['MON'], l['TUE'], l['WED'], l['THU'], l['FRI'], l['SAT'], l['SUN']][+selTime[0]] + tForm}</Button>
125138
<List>
126139
{#each [0, 1, 2, 3, 4] as date}
127140
{#each timeSegments as [s, e]}
@@ -130,15 +143,15 @@
130143
{@const eh = Math.floor(e / 60)}
131144
{@const em = (e % 60).toString().padStart(2, '0')}
132145
{@const key = `${date}-${s}`}
133-
<OneLine title="{['', '', '', '', ''][date]} {sh}:{sm} - {eh}:{em}"
146+
<OneLine title="{[l['MON'], l['TUE'], l['WED'], l['THU'], l['FRI'], l['SAT'], l['SUN']][date]} {sh}:{sm} - {eh}:{em}"
134147
on:click={() => selTime = (selTime === key ? null : key)}
135148
active={selTime === key}/>
136149
{/each}
137150
{/each}
138151
</List>
139152
</Paper>
140153
<Button outlined round icon={way %2 ? "arrow_downward" : "arrow_upward"} small style="margin-left: 4px"
141-
on:click={() => way++}>{['과목코드', '과목코드', '경쟁률', '경쟁률'][way]}</Button>
154+
on:click={() => way++}>{[l['CODE'], l['CODE'], l['COMPETITIVE'], l['COMPETITIVE']][way]}</Button>
142155
</div>
143156
</Paginator>
144157
</div>
@@ -148,17 +161,17 @@
148161
{:else}
149162
<Table minWidth="900">
150163
<tr>
151-
<Th width="3.2">학과</Th>
152-
<Th width="1.8">학점</Th>
153-
<Th width="3.6">코드</Th>
154-
<Th width="2.4">교수</Th>
155-
<Th width="6">과목 이름</Th>
156-
<Th width="4.4">수업 시간</Th>
164+
<Th width="3.2">{l['DEPT']}</Th>
165+
<Th width="1.8">{l['CREDIT']}</Th>
166+
<Th width="3.6">{l['CODE']}</Th>
167+
<Th width="2.4">{l['PROF']}</Th>
168+
<Th width="6">{l['NAME']}</Th>
169+
<Th width="4.4">{l['TIME']}</Th>
157170
{#if compete}
158-
<Th width="4.2">경쟁률</Th>
171+
<Th width="4.2">{l['COMPETITIVE']}</Th>
159172
{/if}
160-
<Th width="2.7">유형</Th>
161-
<Th width="3.8">실라버스/OTL</Th>
173+
<Th width="2.7">{l['TYPE']}</Th>
174+
<Th width="3.8">{l['SYLLABUS']}/OTL</Th>
162175
</tr>
163176
{#each [...favorites, ...selected, ..._list.slice((page - 1) * itemPerPage, page * itemPerPage).filter(i => !selected.includes(i))] as lect, i}
164177
{@const background = i < favorites.length ? 'var(--primary-light6)' : (selected.includes(lect) ? 'var(--secondary-light6)' : '')}
@@ -201,7 +214,7 @@
201214
<TableTd data={lect} bind:hover on:choose {background}>
202215
{#each lect.time as time}
203216
<p>
204-
{['', '', '', '', '', '', ''][time.date]}
217+
{[l['MON'], l['TUE'], l['WED'], l['THU'], l['FRI'], l['SAT'], l['SUN']][time.date]}
205218
{`${time.sh}:${time.sm.toString().padStart(2, '0')} - ${time.eh}:${time.em.toString().padStart(2, '0')}`}
206219
</p>
207220
{/each}
@@ -224,7 +237,7 @@
224237
{#if lect.kcode}
225238
<a href="https://cais.kaist.ac.kr/syllabusInfo?year={year}&term={term}&subject_no={lect.kcode}&dept_id={lect.dept}&lecture_class={lect.group}"
226239
target="_blank" on:click|stopPropagation>
227-
<IconButton description size="18" tooltip="실라버스" right xstack bottom/>
240+
<IconButton description size="18" tooltip={l['SYLLABUS']} right xstack bottom/>
228241
</a>
229242
{/if}
230243
{#if otlMap(lect.code)}

src/lib/Other.svelte

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,26 @@
55
<script lang="ts">
66
import {Card, Icon, IconButton} from "nunui";
77
import {browser} from "$app/environment";
8-
import otlMap from "$lib/otlMap";
8+
import {locale, str} from "$lib";
99
1010
export let detail;
1111
let otherMap = {};
1212
13-
if (!pr && browser) pr = fetch('/other.json').then(r => r.json())
13+
if (!pr && browser) pr = fetch(`${$locale}/other.json`).then(r => r.json())
1414
1515
$: if (pr) pr.then(r => otherMap = r)
1616
1717
$: defTitle = otherMap[detail.code]?.[0]
18+
19+
$: l = str[$locale]
1820
</script>
1921

2022
<Card primary flat>
2123
<div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 12px">
2224
<span style="font-size: 1.2em">{detail.code} {detail.title}</span>
2325
<IconButton close on:click={() => detail = null}/>
2426
</div>
25-
<p>누르면 실라버스로 이동합니다.</p>
27+
<p>{l['CLICK_TO_OPEN_SYLLABUS']}</p>
2628
{#if defTitle}
2729
{#each otherMap[detail.code][1] as [key, history]}
2830
{@const term = key % 10}

src/lib/TableItem.svelte

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import {Button, Icon, List, OneLine, Paper, Tooltip} from "nunui";
44
import otlMap from "$lib/otlMap";
55
import {textfit} from 'svelte-textfit';
6+
import { locale, str } from "$lib";
67
78
let parent;
89
@@ -76,6 +77,8 @@
7677
}
7778
return `color-mix(in srgb, ${inter[inter.length - 1][1]}, var(--surface) 50%)`;
7879
}
80+
81+
$: l = str[$locale]
7982
</script>
8083

8184
{#each data.time as time, i}
@@ -111,7 +114,7 @@
111114
{`${time.sh}:${time.sm.toString().padStart(2, '0')} - ${time.eh}:${time.em.toString().padStart(2, '0')}`}
112115
</p>
113116
{#if compete && !capturing}
114-
<p style="font-size: 0.6em;font-weight: 300;opacity: 0.6;white-space: normal;background:{vscolor(vsRaw || 0)};border-radius: 2px;display:inline-block;padding: 2px">경쟁률 {vs}</p>
117+
<p style="font-size: 0.6em;font-weight: 300;opacity: 0.6;white-space: normal;background:{vscolor(vsRaw || 0)};border-radius: 2px;display:inline-block;padding: 2px">{l['COMPETITIVE']} {vs}</p>
115118
{/if}
116119
</div>
117120
<main style="padding: 12px">
@@ -126,7 +129,7 @@
126129
</p>
127130
{#if data.credit || data.au}
128131
<p style="font-weight: 300;font-size: 0.8em">
129-
<Icon readiness_score/>{data.credit}학점
132+
<Icon readiness_score/>{data.credit}{l['CREDIT']}
130133
{#if data.au}
131134
/ {data.au} AU
132135
{/if}
@@ -142,14 +145,14 @@
142145
{/if}
143146
{#if compete}
144147
<p style="font-weight: 300;font-size: 0.8em">
145-
<Icon account_circle/>{data.reg}/{data.cap}명 신청</p>
148+
<Icon account_circle/>{data.reg}/{data.cap}{l['N_APPLY']}</p>
146149
{/if}
147150
<p style="font-weight: 300;font-size: 0.8em">
148-
<span><Icon timer/>시간</span><br>
151+
<span><Icon timer/>{l['TIME']}</span><br>
149152
<ul>
150153
{#each data.time as time}
151154
<li>
152-
{['', '', '', '', '', '', ''][time.date]}
155+
{[l['MON'], l['TUE'], l['WED'], l['THU'], l['FRI'], l['SAT'], l['SUN']][time.date]}
153156
{`${time.sh}:${time.sm.toString().padStart(2, '0')} - ${time.eh}:${time.em.toString().padStart(2, '0')}`}
154157
</li>
155158
{/each}
@@ -160,34 +163,34 @@
160163
<article style="font-size: 16px;font-weight: 300;margin: 0 -12px">
161164
<List>
162165
{#if data.code}
163-
<OneLine icon="list" title="과목 개설 내역" on:click={() => detail = data}/>
166+
<OneLine icon="list" title={l['HISTORY']} on:click={() => detail = data}/>
164167
<a href="https://cais.kaist.ac.kr/syllabusInfo?year={year}&term={term}&subject_no={data.kcode}&dept_id={data.dept}&lecture_class={data.group}"
165168
target="_blank" on:click|stopPropagation>
166-
<OneLine icon="description" title="실라버스"/>
169+
<OneLine icon="description" title={l['SYLLABUS']}/>
167170
</a>
168171
<a href="https://otl.sparcs.org/dictionary?startCourseId={otlMap(data.code)}"
169172
target="_blank" on:click|stopPropagation>
170-
<OneLine icon="open_in_new" title="OTL 평가"/>
173+
<OneLine icon="open_in_new" title="OTL"/>
171174
</a>
172175
{/if}
173-
<OneLine icon="close" title="삭제" on:click={() => dispatch('remove')}/>
176+
<OneLine icon="close" title={l['REMOVE']} on:click={() => dispatch('remove')}/>
174177
</List>
175178
</article>
176179
{:else if data.code}
177180
<Button small icon="list" on:click={(e) => {
178181
e.stopPropagation()
179182
detail = data
180-
}}>과목 개설 내역
183+
}}>{l['HISTORY']}
181184
</Button>
182185
<a href="https://cais.kaist.ac.kr/syllabusInfo?year={year}&term={term}&subject_no={data.kcode}&dept_id={data.dept}&lecture_class={data.group}"
183186
target="_blank" on:click|stopPropagation>
184-
<Button small icon="description">실라버스</Button>
187+
<Button small icon="description">{l['SYLLABUS']}</Button>
185188
</a>
186189
{/if}
187190
{#if !mobile && otlMap(data.code)}
188191
<a href="https://otl.sparcs.org/dictionary?startCourseId={otlMap(data.code)}" target="_blank"
189192
on:click|stopPropagation>
190-
<Button small icon="open_in_new">OTL 평가</Button>
193+
<Button small icon="open_in_new">OTL</Button>
191194
</a>
192195
{/if}
193196
</main>

0 commit comments

Comments
 (0)