diff --git a/README.md b/README.md index a230ac41..841ba1f6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@

muetab.com

-
[![Chrome](./assets/chrome.svg)](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid) @@ -15,9 +14,8 @@
- - ## 🤔 Why Mue? + - Beautiful and Minimalist Design - Customisable Layout - Widgets (such as weather, notes, bookmarks and more) @@ -26,6 +24,7 @@ - Open Source under the BSD-3 License ## 🌶️ Installation + Mue can be downloaded on the following browsers: - [Chrome](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid) @@ -35,21 +34,23 @@ Mue can be downloaded on the following browsers: and can be manually sideloaded on others using the files on [GitHub Releases](https://github.com/mue/mue/releases) - ## 🚀 Demo + A fully-featured demo of the tab extension is available in-browser at [demo.muetab.com](https://demo.muetab.com) - ## 💻 Development -Install dependencies with ``bun install``, and then you can run any of the following scripts as needed: + +Install dependencies with `bun install`, and then you can run any of the following scripts as needed: - `bun run dev[:host]` - start development server - `bun run build` - build production copy of Mue - `bun run lint[:fix]` - run linter - `bun run pretty` - run prettier - `bun run translations` - migrate old translation format to new +- `bun run translations:percentages` - update translation completion percentages from Weblate ## 🐳 Docker development + Hot reload is available while coding. - `docker build -t mue-app .` - build the image @@ -61,14 +62,16 @@ Hot reload is available while coding. - `docker run -p 5173:5173 \ -v $(pwd):/app \ -v dev-bun-app:/app/node_modules \ - mue-app + mue-app ` - run the container Navigate to http://localhost:5173 ## 🌍 Translations + We use [Weblate](https://weblate.org) for translations. To get started, please visit the [project](https://hosted.weblate.org/projects/mue/) and look for the latest version to start translating Mue into your langauge. ## Attribution + [Pexels](https://pexels.com), [Unsplash](https://unsplash.com) - Stock photos used for offline mode
[Undraw](https://undraw.co) - Welcome modal images diff --git a/package.json b/package.json index 61592263..0a0b7d6b 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "dev": "vite", "dev:host": "vite --host", "translations": "cd scripts && node updatetranslations.cjs", + "translations:percentages": "node scripts/updateTranslationPercentages.cjs", "build": "vite build", "pretty": "prettier --write \"./**/*.{js,jsx,json,scss,css}\"", "lint": "eslint \"./src/**/*.{js,jsx}\" && stylelint \"./src/**/*.{scss,css}\"", diff --git a/scripts/updateTranslationPercentages.cjs b/scripts/updateTranslationPercentages.cjs new file mode 100644 index 00000000..e2eef21c --- /dev/null +++ b/scripts/updateTranslationPercentages.cjs @@ -0,0 +1,91 @@ +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +// Language code mappings between Weblate and Mue +const CODE_MAPPINGS = { + de: 'de_DE', + id: 'id_ID', + nb_NO: 'no', + tr: 'tr_TR', + zh_Hans: 'zh_CN', + pt: 'pt_PT', +}; + +const WEBLATE_API_URL = 'https://hosted.weblate.org/api/components/mue/mue-tab-7-1/statistics/'; + +function fetchWeblateStats() { + return new Promise((resolve, reject) => { + const options = { + headers: { + 'User-Agent': 'Mue-Translation-Update-Script', + Accept: 'application/json', + }, + }; + + https + .get(WEBLATE_API_URL, options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const json = JSON.parse(data); + resolve(json); + } catch (e) { + reject(e); + } + }); + }) + .on('error', (err) => { + reject(err); + }); + }); +} + +function mapLanguageCode(weblateCode) { + return CODE_MAPPINGS[weblateCode] || weblateCode; +} + +async function updateTranslationPercentages() { + try { + console.log('Fetching translation statistics from Weblate...'); + const data = await fetchWeblateStats(); + + const percentages = {}; + + data.results.forEach((lang) => { + const code = mapLanguageCode(lang.code); + percentages[code] = { + percent: Math.round(lang.translated_percent * 10) / 10, // Round to 1 decimal place + lastChange: lang.last_change, + }; + }); + + const outputPath = path.join(__dirname, '../src/i18n/translationPercentages.json'); + fs.writeFileSync(outputPath, JSON.stringify(percentages, null, 2)); + fs.appendFileSync(outputPath, '\n'); + + console.log(`✓ Translation percentages updated successfully!`); + console.log(` Total languages: ${Object.keys(percentages).length}`); + console.log(` Output: ${outputPath}`); + + // Show some examples + const sortedByPercent = Object.entries(percentages) + .sort((a, b) => b[1].percent - a[1].percent) + .slice(0, 5); + + console.log('\nTop 5 translated languages:'); + sortedByPercent.forEach(([code, data]) => { + console.log(` ${code}: ${data.percent}%`); + }); + } catch (error) { + console.error('Error updating translation percentages:', error); + process.exit(1); + } +} + +updateTranslationPercentages(); diff --git a/src/components/Elements/MainModal/scss/settings/modules/tabs/_order.scss b/src/components/Elements/MainModal/scss/settings/modules/tabs/_order.scss index ecc935c6..02a8460b 100644 --- a/src/components/Elements/MainModal/scss/settings/modules/tabs/_order.scss +++ b/src/components/Elements/MainModal/scss/settings/modules/tabs/_order.scss @@ -46,7 +46,7 @@ .images-row { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); padding: 20px; grid-gap: 20px; diff --git a/src/contexts/TranslationContext.jsx b/src/contexts/TranslationContext.jsx index 12ec331a..c9fa1d1b 100644 --- a/src/contexts/TranslationContext.jsx +++ b/src/contexts/TranslationContext.jsx @@ -1,4 +1,12 @@ -import { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import { + createContext, + useContext, + useState, + useEffect, + useCallback, + useMemo, + useRef, +} from 'react'; import { initTranslations, translations } from 'lib/translations'; import variables from 'config/variables'; import EventBus from 'utils/eventbus'; @@ -20,37 +28,47 @@ export function TranslationProvider({ children, initialLanguage }) { }, [currentLanguage, initialLanguage]); // Change language function - const changeLanguage = useCallback((newLanguage) => { - // Update the i18n instance - i18nInstance.current = initTranslations(newLanguage); - variables.language = i18nInstance.current; - variables.languagecode = newLanguage; - document.documentElement.lang = newLanguage.replace('_', '-'); + const changeLanguage = useCallback( + (newLanguage) => { + // Update the i18n instance + i18nInstance.current = initTranslations(newLanguage); + variables.language = i18nInstance.current; + variables.languagecode = newLanguage; + document.documentElement.lang = newLanguage.replace('_', '-'); - // Update tab name if it's still the default - const currentTabName = localStorage.getItem('tabName'); - const oldDefaultTabName = i18nInstance.current?.getMessage(currentLanguage, 'tabname'); + // Update tab name if it's still the default + const currentTabName = localStorage.getItem('tabName'); + const oldDefaultTabName = i18nInstance.current?.getMessage(currentLanguage, 'tabname'); - if (currentTabName === oldDefaultTabName || !currentTabName) { - const newTabName = translations[newLanguage.replace('-', '_')]?.tabname || - i18nInstance.current?.getMessage(newLanguage, 'tabname') || - 'Mue'; - localStorage.setItem('tabName', newTabName); - document.title = newTabName; - } + if (currentTabName === oldDefaultTabName || !currentTabName) { + const newTabName = + translations[newLanguage.replace('-', '_')]?.tabname || + i18nInstance.current?.getMessage(newLanguage, 'tabname') || + 'Mue'; + localStorage.setItem('tabName', newTabName); + document.title = newTabName; + } - // Update language in localStorage - localStorage.setItem('language', newLanguage); + // Update language in localStorage + localStorage.setItem('language', newLanguage); - // Update state to trigger re-render - setCurrentLanguage(newLanguage); - }, [currentLanguage]); + // Clear weather cache so it refreshes with the new language + localStorage.removeItem('currentWeather'); + + // Update state to trigger re-render + setCurrentLanguage(newLanguage); + }, + [currentLanguage], + ); // Single translation function - the main API - const t = useCallback((key, optional = {}) => { - if (!i18nInstance.current) return key; - return i18nInstance.current.getMessage(currentLanguage, key, optional); - }, [currentLanguage]); + const t = useCallback( + (key, optional = {}) => { + if (!i18nInstance.current) return key; + return i18nInstance.current.getMessage(currentLanguage, key, optional); + }, + [currentLanguage], + ); // Listen for EventBus language change events (for backward compatibility) useEffect(() => { @@ -72,18 +90,17 @@ export function TranslationProvider({ children, initialLanguage }) { variables.getMessage = (key, optional = {}) => t(key, optional); }, [t]); - const value = useMemo(() => ({ - language: currentLanguage, - languagecode: currentLanguage, // Alias for backward compatibility - changeLanguage, - t, - }), [currentLanguage, changeLanguage, t]); - - return ( - - {children} - + const value = useMemo( + () => ({ + language: currentLanguage, + languagecode: currentLanguage, // Alias for backward compatibility + changeLanguage, + t, + }), + [currentLanguage, changeLanguage, t], ); + + return {children}; } export function useTranslation() { diff --git a/src/features/background/options/Custom.jsx b/src/features/background/options/Custom.jsx index 2d433ce5..1853415f 100644 --- a/src/features/background/options/Custom.jsx +++ b/src/features/background/options/Custom.jsx @@ -148,19 +148,22 @@ const CustomSettings = memo(() => { setCurrentBackgroundIndex(newIndex); }, [customBackground.length]); - const addCustomURL = useCallback(async (e) => { - // regex: https://ihateregex.io/expr/url/ - const urlRegex = - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/; - if (urlRegex.test(e) === false) { - return setUrlError(variables.getMessage('widgets.quicklinks.url_error')); - } + const addCustomURL = useCallback( + async (e) => { + // regex: https://ihateregex.io/expr/url/ + const urlRegex = + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/; + if (urlRegex.test(e) === false) { + return setUrlError(variables.getMessage('widgets.quicklinks.url_error')); + } - const newIndex = customBackground.length; - setCustomURLModal(false); - setCurrentBackgroundIndex(newIndex); - await handleCustomBackground({ target: { result: e } }, newIndex); - }, [customBackground.length, handleCustomBackground]); + const newIndex = customBackground.length; + setCustomURLModal(false); + setCurrentBackgroundIndex(newIndex); + await handleCustomBackground({ target: { result: e } }, newIndex); + }, + [customBackground.length, handleCustomBackground], + ); useEffect(() => { const dnd = customDnd.current; @@ -229,7 +232,7 @@ const CustomSettings = memo(() => { dnd.ondrop = null; } }; - }, [currentBackgroundIndex, handleCustomBackground]); + }, [customBackground.length, handleCustomBackground]); const hasVideo = customBackground.filter((bg) => bg && videoCheck(bg)).length > 0; @@ -265,9 +268,7 @@ const CustomSettings = memo(() => { type="settings" onClick={uploadCustomBackground} icon={} - label={variables.getMessage( - 'modals.main.settings.sections.background.source.upload', - )} + label={variables.getMessage('modals.main.settings.sections.background.source.upload')} />