diff --git a/.eslintrc.js b/.eslintrc.js index c8afe7159..9c8a46b35 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,7 @@ module.exports = { { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], rules: { + 'prettier/prettier': ['error', { 'endOfLine': 'auto' }], '@typescript-eslint/no-shadow': ['warn'], 'no-shadow': 'off', 'no-undef': 'off', diff --git a/src/database/queries/CategoryQueries.ts b/src/database/queries/CategoryQueries.ts index ca2afa09a..c56666afe 100644 --- a/src/database/queries/CategoryQueries.ts +++ b/src/database/queries/CategoryQueries.ts @@ -72,8 +72,10 @@ export const isCategoryNameDuplicate = ( tx.executeSql( isCategoryNameDuplicateQuery, [categoryName], - (txObj, { rows: { _array } }) => - resolve(Boolean(_array[0]?.isDuplicate)), + (txObj, { rows }) => { + const { _array } = rows as any; + resolve(Boolean(_array[0]?.isDuplicate)); + }, txnErrorCallback, ); }), diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index b15dd3297..2a795cb04 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -6,6 +6,7 @@ import { ChapterItem } from '../types'; import * as cheerio from 'cheerio'; import RNFetchBlob from 'rn-fetch-blob'; import { txnErrorCallback } from '@database/utils/helpers'; +import { LoadingImageSrc } from '@screens/reader/utils/LoadImage'; const db = SQLite.openDatabase('lnreader.db'); @@ -243,13 +244,15 @@ const createImageFolder = async ( }, ): Promise => { const mkdirIfNot = async (p: string) => { + const nomediaPath = + p + (p.charAt(p.length - 1) === '/' ? '' : '/') + '.nomedia'; if (!(await RNFetchBlob.fs.exists(p))) { await RNFetchBlob.fs.mkdir(p); + await RNFetchBlob.fs.createFile(nomediaPath, ',', 'utf8'); } }; await mkdirIfNot(path); - await RNFetchBlob.fs.createFile(path, '.nomedia', 'utf8'); if (data) { const { sourceId, novelId, chapterId } = data; @@ -269,31 +272,55 @@ const downloadImages = async ( chapterId: number, ): Promise => { try { + const headers = sourceManager(sourceId)?.headers || {}; const loadedCheerio = cheerio.load(html); const imgs = loadedCheerio('img').toArray(); for (let i = 0; i < imgs.length; i++) { const elem = loadedCheerio(imgs[i]); const url = elem.attr('src'); if (url) { - const imageb64 = (await RNFetchBlob.fetch('GET', url)).base64(); + const imageb64 = ( + await RNFetchBlob.fetch('GET', url, headers) + ).base64(); const fileurl = (await createImageFolder( `${RNFetchBlob.fs.dirs.DownloadDir}/LNReader`, { sourceId, novelId, chapterId }, - )) + + ).catch(() => { + showToast( + `Unexpected storage error!\nRemove ${fileurl} and try downloading again`, + ); + return '--'; + })) + i + '.b64.png'; + if (fileurl.charAt(0) === '-') { + return loadedCheerio.html(); + } elem.replaceWith( - ``, + ``, ); - const exists = await RNFetchBlob.fs.exists(fileurl); + const exists = await RNFetchBlob.fs.exists(fileurl).catch(() => { + showToast( + `Unexpected storage error!\nRemove ${fileurl} and try downloading again`, + ); + }); if (!exists) { - RNFetchBlob.fs.createFile(fileurl, imageb64, 'utf8'); + RNFetchBlob.fs.createFile(fileurl, imageb64, 'base64').catch(() => { + showToast( + `Unexpected storage error!\nRemove ${fileurl} and try downloading again`, + ); + }); } else { - RNFetchBlob.fs.writeFile(fileurl, imageb64, 'utf8'); + RNFetchBlob.fs.writeFile(fileurl, imageb64, 'base64').catch(() => { + showToast( + `Unexpected storage error!\nRemove ${fileurl} and try downloading again`, + ); + }); } } } + loadedCheerio('body').prepend(""); return loadedCheerio.html(); } catch (e) { return html; @@ -350,20 +377,16 @@ const deleteDownloadedImages = async ( chapterId: number, ) => { try { - const path = `${RNFetchBlob.fs.dirs.DownloadDir}/LNReader/`; - const files = await RNFetchBlob.fs.ls( - await createImageFolder(path, { sourceId, novelId, chapterId }), + const path = await createImageFolder( + `${RNFetchBlob.fs.dirs.DownloadDir}/LNReader`, + { sourceId, novelId, chapterId }, ); + const files = await RNFetchBlob.fs.ls(path); for (let i = 0; i < files.length; i++) { - const ex = /(.*?)_(.*?)#(.*?)/.exec(files[i]); + const ex = /\.b64\.png/.exec(files[i]); if (ex) { - if ( - parseInt(ex[1], 10) === sourceId && - parseInt(ex[2], 10) === chapterId - ) { - if (await RNFetchBlob.fs.exists(`${path}${files[i]}`)) { - RNFetchBlob.fs.unlink(`${path}${files[i]}`); - } + if (await RNFetchBlob.fs.exists(`${path}${files[i]}`)) { + RNFetchBlob.fs.unlink(`${path}${files[i]}`); } } } diff --git a/src/database/queries/DownloadQueries.ts b/src/database/queries/DownloadQueries.ts index 772c5c2d1..f099ae364 100644 --- a/src/database/queries/DownloadQueries.ts +++ b/src/database/queries/DownloadQueries.ts @@ -79,7 +79,10 @@ export const getChapterFromDb = async ( getChapterFromDbQuery, [chapterId], (txObj, results) => resolve(results.rows.item(0)), - (txObj, error) => reject(error), + (_, error) => { + reject(error); + return false; + }, ); }), ); diff --git a/src/redux/source/sourcesSlice.ts b/src/redux/source/sourcesSlice.ts index e607b5e6e..7eae4f5da 100644 --- a/src/redux/source/sourcesSlice.ts +++ b/src/redux/source/sourcesSlice.ts @@ -1,8 +1,34 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import AllSources from '../../sources/sources.json'; +import { translations } from 'i18n-js'; +import { localization } from '../../../strings/translations'; import { Source } from '../../sources/types'; +const languages = { + 'en': 'English', + 'es': 'Spanish', + 'tr': 'Turkish', + 'ru': 'Russian', + 'ar': 'Arabic', + 'uk': 'Ukrainian', + 'pt': 'Portuguese', + 'pt-BR': 'Portuguese', + 'de': 'German', + 'it': 'Italian', + 'zh-CN': 'Chinese', + 'zh-TW': 'Chinese', + 'vi': 'Vietnamese', + 'ja': 'Japanese', +}; +// Hope you can do something to synchronize translation system. +// Also take care of some case like vi-VN: Vietnamese not vi +export const defaultLanguage = + localization in translations || localization.split('-')[0] in translations + ? languages[localization] || languages[localization.split('-')[0]] + : 'English'; +// That's why this code is so long :( + interface SourcesState { allSources: Source[]; searchResults: Source[]; @@ -16,7 +42,7 @@ const initialState: SourcesState = { searchResults: [], pinnedSourceIds: [], lastUsed: null, - languageFilters: ['English'], + languageFilters: [defaultLanguage], }; export const sourcesSlice = createSlice({ diff --git a/src/screens/browse/BrowseScreen.tsx b/src/screens/browse/BrowseScreen.tsx index ee049f3ce..bb4f051a8 100644 --- a/src/screens/browse/BrowseScreen.tsx +++ b/src/screens/browse/BrowseScreen.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { EmptyView, SearchbarV2 } from '../../components'; import { + defaultLanguage, getSourcesAction, searchSourcesAction, setLastUsedSource, @@ -42,7 +43,7 @@ const BrowseScreen = () => { allSources, searchResults, pinnedSourceIds = [], - languageFilters = ['English'], + languageFilters = [defaultLanguage || 'English'], //defaultLang cant be null, but just for sure lastUsed, } = useSourcesReducer(); diff --git a/src/screens/novel/NovelScreen.js b/src/screens/novel/NovelScreen.js index 33cb84707..904e7f34c 100644 --- a/src/screens/novel/NovelScreen.js +++ b/src/screens/novel/NovelScreen.js @@ -140,8 +140,6 @@ const Novel = ({ route, navigation }) => { const [jumpToChapterModal, showJumpToChapterModal] = useState(false); - const keyExtractor = useCallback(i => i.chapterId.toString(), []); - const downloadChapter = (chapterUrl, chapterName, chapterId) => dispatch( downloadChapterAction( @@ -585,12 +583,12 @@ const Novel = ({ route, navigation }) => { estimatedItemSize={64} data={!loading && chapters} extraData={[downloadQueue, selected]} - keyExtractor={keyExtractor} removeClippedSubviews={true} maxToRenderPerBatch={5} windowSize={15} initialNumToRender={7} renderItem={renderItem} + keyExtractor={(item, index) => 'chapter' + index} contentContainerStyle={{ paddingBottom: 100 }} ListHeaderComponent={ { contentContainerStyle={styles.genreContainer} horizontal data={data} - keyExtractor={item => item} + keyExtractor={(item, index) => 'genre' + index} renderItem={renderItem} showsHorizontalScrollIndicator={false} /> diff --git a/src/screens/reader/ReaderScreen.js b/src/screens/reader/ReaderScreen.js index 6a24f40a8..686ab1461 100644 --- a/src/screens/reader/ReaderScreen.js +++ b/src/screens/reader/ReaderScreen.js @@ -264,22 +264,22 @@ const ChapterContent = ({ route, navigation }) => { }; const navigateToChapterBySwipe = name => { - let chapter; + let navChapter; if (name === 'SWIPE_LEFT') { - chapter = nextChapter; + navChapter = nextChapter; } else if (name === 'SWIPE_RIGHT') { - chapter = prevChapter; + navChapter = prevChapter; } else { return; } // you can add more condition for friendly usage. for example: if(name === "SWIPE_LEFT" || name === "right") - chapter + navChapter ? navigation.replace('Chapter', { ...params, - chapterUrl: chapter.chapterUrl, - chapterId: chapter.chapterId, - chapterName: chapter.chapterName, - bookmark: chapter.bookmark, + chapterUrl: navChapter.chapterUrl, + chapterId: navChapter.chapterId, + chapterName: navChapter.chapterName, + bookmark: navChapter.bookmark, }) : showToast( name === 'SWIPE_LEFT' @@ -299,6 +299,7 @@ const ChapterContent = ({ route, navigation }) => { const chapterText = sanitizeChapterText(chapter.chapterText, { removeExtraParagraphSpacing, + sourceId: sourceId, }); const openDrawer = () => { navigation.openDrawer(); @@ -316,7 +317,7 @@ const ChapterContent = ({ route, navigation }) => { return ( <> = ({ const backgroundColor = tabHeaderColor; const renderScene = SceneMap({ - first: ReaderTab, - second: GeneralTab, + 'readerTab': ReaderTab, + 'generalTab': GeneralTab, }); const layout = useWindowDimensions(); @@ -145,11 +145,11 @@ const ReaderBottomSheetV2: React.FC = ({ const routes = useMemo( () => [ { - key: 'first', + key: 'readerTab', title: getString('moreScreen.settingsScreen.readerSettings.title'), }, { - key: 'second', + key: 'generalTab', title: getString('moreScreen.settingsScreen.generalSettings'), }, ], diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx index b66454cf5..4a0c6ae7b 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx @@ -27,6 +27,7 @@ const ReaderFontPicker = () => { 'font' + index} horizontal={true} showsHorizontalScrollIndicator={false} /> diff --git a/src/screens/reader/components/WebViewReader.tsx b/src/screens/reader/components/WebViewReader.tsx index 9c649ea54..c08c51c29 100644 --- a/src/screens/reader/components/WebViewReader.tsx +++ b/src/screens/reader/components/WebViewReader.tsx @@ -8,17 +8,28 @@ import { ChapterItem } from '@database/types'; import { useReaderSettings } from '@redux/hooks'; import { getString } from '@strings/translations'; +import { sourceManager } from '../../../sources/sourceManager'; + type WebViewPostEvent = { type: string; data?: { [key: string]: string }; }; type WebViewReaderProps = { - chapter: ChapterItem; + chapterInfo: { + sourceId: number; + chapterId: string; + chapterUrl: string; + novelId: string; + novelUrl: string; + novelName: string; + chapterName: string; + bookmark: string; + }; html: string; chapterName: string; swipeGestures: boolean; - minScroll: any; + minScroll: React.MutableRefObject; nextChapter: ChapterItem; webViewRef: React.MutableRefObject; onPress(): void; @@ -35,7 +46,7 @@ const onClickWebViewPostMessage = (event: WebViewPostEvent) => const WebViewReader: React.FC = props => { const { - chapter, + chapterInfo, html, chapterName, swipeGestures, @@ -55,7 +66,7 @@ const WebViewReader: React.FC = props => { const { theme: backgroundColor } = readerSettings; const layoutHeight = Dimensions.get('window').height; - + const headers = sourceManager(chapterInfo.sourceId)?.headers; return ( = props => { break; case 'imgfiles': if (event.data) { - if (Array.isArray(event.data)) { + if (Array.isArray(event.data.imgs)) { const promises: Promise<{ data: any; id: number }>[] = []; - for (let i = 0; i < event.data.length; i++) { - const { url, id } = event.data[i]; - if (url) { - if (id) { + if (event.data.type === 'online') { + for (let i = 0; i < event.data.imgs.length; i++) { + const { url, id } = event.data.imgs[i]; + if (url && id) { promises.push( - RNFetchBlob.fs - .readFile(url, 'utf8') - .then(d => ({ data: d, id })), + RNFetchBlob.fetch('get', url, headers).then(res => ({ + data: res.base64(), + id: id, + })), + ); + } + } + } else { + for (let i = 0; i < event.data.imgs.length; i++) { + const { url, id } = event.data.imgs[i]; + if (url && id) { + promises.push( + RNFetchBlob.fs.readFile(url, 'base64').then(base64 => { + return { data: base64, id: id }; + }), ); - } else { - // no imageid } } } + Promise.all(promises) .then(datas => { const inject = datas.reduce((p, data) => { return ( p + - `document.querySelector("img[file-id='${data.id}']").src="data:image/png;base64,${data.data}";` + `document.querySelector("img[file-id='${data.id}']").setAttribute("src", "data:image/jpg;base64,${data.data}"); + document.querySelector("img[file-id='${data.id}']").classList.remove("load-icon");` ); }, ''); - webViewRef.current?.injectJavaScript(inject); + webViewRef.current?.injectJavaScript( + inject + + 'window.requestAnimationFrame(()=>sendHeight());', + ); }) .catch(e => { e; // CloudFlare is too strong :D @@ -166,6 +192,19 @@ const WebViewReader: React.FC = props => { height: auto; max-width: 100%; } + img.load-icon { + display: block; + margin-inline: auto; + animation: rotation 1s infinite linear; + } + @keyframes rotation { + 100% { + transform: rotate(360deg); + } + 0% { + transform: rotate(0deg); + } + } .nextButton, .infoText { width: 100%; @@ -215,19 +254,29 @@ const WebViewReader: React.FC = props => { type: 'hide', })}> ${html}
${getString( diff --git a/src/screens/reader/utils/LoadImage.ts b/src/screens/reader/utils/LoadImage.ts new file mode 100644 index 000000000..c4dfd0b69 --- /dev/null +++ b/src/screens/reader/utils/LoadImage.ts @@ -0,0 +1,2 @@ +export const LoadingImageSrc = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFhklEQVR42u2aS6hdZxXHf7/17X3uTWxjYlrTNLVGSYgVpJBabaAqhUpKRbSSUQdaFJyIk6KYSQQ7sAg6sANFEB9TobSEIqIU8UGkVCypVIuxTTUxTe0zzU2a3HPOXg7OKTm9NOamnseOnD/sycdh77Ne/7W+tRbMMcccc8wxxxzTRFVVAqWqSlHLLP+L0/hIRFTqun6/v0PdnpnbgU3qmszsRvigxs/7/X7v/8bKpUQAVwGfV+8HjgKvAT2gAXLkOanuWVxc9JIXPCJKVZUd6reAI0B3hbBv+qj3l1KqS1l2gXeq9wDH38TKF3p+DJRL1uOBW4HHgP4qBW6GTw94Rv3gTAh5DO/oAF8Cvg6sP89vGuAs8IJ6CPhbZh4DloDTwK+qqjrc7Xb/64fqurbb7Wab4n0R+DZw5jxWXgYOqnerNwLrSomOGm+NWMt69Yq6rltBlgvAd89Dcj3gUeDOUsrlYyMZtaqqzerOUmKmhFkB9wwtvFL459WvARsmVWdExOaI2A0szortPzuM3TeQmvpYRNwUMfnqTt0SEXfOQgnvH+b3lcL/uqqqrVP+L9epn4uYXjgsAD9bkeMb9UBd1++auiuqwMfV2zudKRCjevuwnB21/tOlxHUzq760o34FmKwBSokF9eEVwp9S7yilzDotvVfdN9FSWr1lBfE16g/UugXX61D3AjsnWer+cEXsHwO2teYiou9T71UvygtWVZFF+A5g90heT+CnwNMtuo88lZlXAFvHrgDwQ8CmkYNX1R8NPaIVyMyu+jvgtospwFajAJum+fDoxUl9RH2mdfdxOZCZH7uYMIhVxFalXj+i1SYzf9E0TevaV5kcUzdm5tvH6QGxIsd21T8OeaBtWM7M59QdY1NAZi5k5uaRo9dKKU+2tDHTqEcvJjtVqwiBNZm5ZuTo5V6vd6aN0mdmA/ybQTN2PB4Q4co2zQvqMu1FqJevtjq9oAL6/SYY9Ple94h+i4VHrTLzsn6/Px4FqKeAEyNutjYzW9m/LyUiMzcCS51OnWNRwDCujo8crRteQ9vHgE2GerX6yvJydzwKKCW66sGRow2ZuaGd7k8B3pOZz46NBPv9psnMJ0bK3sWI2NZSD9iYmdeUUv4+1rtAKXGAQesboG6a5oaWhsFH1F5mHhqrApom/wocHkkzt0bYqjleKaVSP5WZB5umWRpvYg1D/eZIP+AlYEebFBDhtcBx9a5JfWMn8OpQAX31G291wjMJ+YF9wL/qut4yqRxbD+f8r3eEjqhbW6KAdwOH1fsmunGi3jziBY1636y5QK3V7w3vAB+YdJxVwPdHuOBkRNw2q65wKUX1k0Oj3Ms09gvUa4C/jITCk+pM6oKqKtuBQ8DjEbF5il7nbcDLnFt0eFjdNE3hO516i/pbBsPYXVNOOVGAL3NuStRXHwK2TMfy1dbhkGYpIu5aWFiYfghGRK3u49xyRAP8Qb0+IpxMzEeUEjvVPwGnI2JvXVczJeGOevdIZkjgqPoFdc2Yv/U29YsMBjJLEfHVqqpmPpVi2IL+BPDUSHY4Czyk3hwRC//j+zvAR4FfDt97JCLu6HTq1q3UbVN/whunx0vqfmBPRFwJ1MM12QuGuXplROwB9jNYpjoDPBgx3km0Y/aGhczcDewFbmCwQQaDPaLngUfVRzLzYCnl2cx8cTjRyaZptqhXATsy8yZgF4NpVAKPA99RH8jMs7Qd6mURfhp4AHiRwdLU6H7gMoNJ8wnglSGHnB6e94e/f0ndr34GWMeE9o0mnT5qdXNm3qLemJm7gGuBtSuqtj5wCviH+ufM/L36m1Lin71evztRY03RKyIz64hY2zTN1aWU9U3TCDSlxIler/+cerKU0uv1en3mmGOOOeaYY4455pgs/gOvX+OnM4R2CQAAAABJRU5ErkJggg=='; diff --git a/src/screens/reader/utils/sanitizeChapterText.ts b/src/screens/reader/utils/sanitizeChapterText.ts index 4e1bf6709..10274e687 100644 --- a/src/screens/reader/utils/sanitizeChapterText.ts +++ b/src/screens/reader/utils/sanitizeChapterText.ts @@ -1,7 +1,12 @@ import sanitizeHtml from 'sanitize-html'; +import { load as loadCheerio } from 'cheerio'; +import { sourceManager } from './../../../sources/sourceManager'; +import { LoadingImageSrc } from './LoadImage'; + interface Options { removeExtraParagraphSpacing?: boolean; + sourceId?: number; } export const sanitizeChapterText = ( @@ -9,10 +14,11 @@ export const sanitizeChapterText = ( options?: Options, ): string => { let text = sanitizeHtml(html, { - allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'input']), allowedAttributes: { - 'img': ['src', 'type', 'file-src', 'file-id'], + 'img': ['src', 'type', 'file-path', 'file-id', 'offline', 'class'], 'a': ['href'], + 'input': ['type', 'offline'], }, allowedSchemes: ['data', 'http', 'https'], }); @@ -21,6 +27,24 @@ export const sanitizeChapterText = ( if (options?.removeExtraParagraphSpacing) { text = text.replace(/<\s*br[^>]*>/gi, '\n').replace(/\n{2,}/g, '\n\n'); } + const loadedCheerio = loadCheerio(text); + if ( + options?.sourceId && + sourceManager(options.sourceId).headers && + loadedCheerio('input[offline]').length === 0 + ) { + loadedCheerio('img').each((i, element) => { + const src = loadedCheerio(element).attr('src'); + if (src) { + loadedCheerio(element).attr({ + 'src': LoadingImageSrc, + 'class': 'load-icon', + 'delayed-src': src, + }); + } + }); + text = loadedCheerio('body').html() || text; + } } else { text = "Chapter is empty.\n\nReport on github if it's available in webview."; diff --git a/src/sources/sourceManager.ts b/src/sources/sourceManager.ts index e2fb9db30..fab883873 100644 --- a/src/sources/sourceManager.ts +++ b/src/sources/sourceManager.ts @@ -157,6 +157,7 @@ interface Scraper { ) => Promise; searchNovels: (searchTerm: string) => Promise; filters?: SourceFilter[]; + headers?: { [key: string]: string }; } export const sourceManager = (sourceId: number): Scraper => { const scrapers: Record = { diff --git a/src/sources/vi/HakoLightNovel.ts b/src/sources/vi/HakoLightNovel.ts index 6ff4e092e..e7b9be7bf 100644 --- a/src/sources/vi/HakoLightNovel.ts +++ b/src/sources/vi/HakoLightNovel.ts @@ -12,8 +12,8 @@ const sourceName = 'HakoLightNovel'; const baseUrl = 'https://ln.hako.vn'; const popularNovels = async (page: number) => { - const totalPages = 49; - const url = `${baseUrl}/danh-sach?page=${page}`; + const totalPages = 59; + const url = `${baseUrl}/danh-sach?truyendich=1&sapxep=topthang&page=${page}`; const result = await fetch(url); const body = await result.text(); @@ -73,7 +73,13 @@ const parseNovelAndChapters = async (novelUrl: string) => { novel.novelName = loadedCheerio('.series-name').text(); - let novelCover = loadedCheerio('.img > img').attr('src'); + const background = loadedCheerio('.series-cover > .a6-ratio > div').attr( + 'style', + ); + const novelCover = background?.substring( + background.indexOf('http'), + background.length - 2, + ); novel.novelCover = novelCover ? isUrlAbsolute(novelCover) @@ -190,11 +196,16 @@ const searchNovels = async (searchTerm: string) => { return novels; }; +const headers = { + Referer: 'https://docln.net/', +}; + const HakoLightNovelScraper = { popularNovels, parseNovelAndChapters, parseChapter, searchNovels, + headers, }; export default HakoLightNovelScraper; diff --git a/strings/translations.ts b/strings/translations.ts index 00b8a68ad..fda611ae4 100644 --- a/strings/translations.ts +++ b/strings/translations.ts @@ -57,7 +57,7 @@ i18n.translations = { }; i18n.locale = Localization.locale; dayjs.locale(Localization.locale); - +export const localization = Localization.locale; export const getString = (stringKey: keyof StringMap) => i18n.t(stringKey); dayjs.Ls[dayjs.locale()].calendar = {