mirror of
https://github.com/mue/mue.git
synced 2026-06-05 23:45:53 +02:00
feat: add translation percentage tracking and update related components
This commit is contained in:
17
README.md
17
README.md
@@ -6,7 +6,6 @@
|
||||
|
||||
<p align="center"><a href="https://muetab.com">muetab.com</a></p>
|
||||
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
@@ -15,9 +14,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
## 🤔 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 <br/>
|
||||
[Undraw](https://undraw.co) - Welcome modal images
|
||||
|
||||
@@ -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}\"",
|
||||
|
||||
91
scripts/updateTranslationPercentages.cjs
Normal file
91
scripts/updateTranslationPercentages.cjs
Normal file
@@ -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();
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<TranslationContext.Provider value={value}>
|
||||
{children}
|
||||
</TranslationContext.Provider>
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
language: currentLanguage,
|
||||
languagecode: currentLanguage, // Alias for backward compatibility
|
||||
changeLanguage,
|
||||
t,
|
||||
}),
|
||||
[currentLanguage, changeLanguage, t],
|
||||
);
|
||||
|
||||
return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
|
||||
}
|
||||
|
||||
export function useTranslation() {
|
||||
|
||||
@@ -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={<MdOutlineFileUpload />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.upload',
|
||||
)}
|
||||
label={variables.getMessage('modals.main.settings.sections.background.source.upload')}
|
||||
/>
|
||||
<Button
|
||||
type="settings"
|
||||
@@ -285,10 +286,7 @@ const CustomSettings = memo(() => {
|
||||
{customBackground.map((url, index) => (
|
||||
<div key={index}>
|
||||
{url && !videoCheck(url) ? (
|
||||
<img
|
||||
alt={'Custom background ' + (index || 0)}
|
||||
src={customBackground[index]}
|
||||
/>
|
||||
<img alt={'Custom background ' + (index || 0)} src={customBackground[index]} />
|
||||
) : url && videoCheck(url) ? (
|
||||
<MdPersonalVideo className="customvideoicon" />
|
||||
) : null}
|
||||
@@ -318,12 +316,9 @@ const CustomSettings = memo(() => {
|
||||
)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.formats',
|
||||
{
|
||||
list: 'jpeg, png, webp, webm, gif, mp4, webm, ogg',
|
||||
},
|
||||
)}
|
||||
{variables.getMessage('modals.main.settings.sections.background.source.formats', {
|
||||
list: 'jpeg, png, webp, webm, gif, mp4, webm, ogg',
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TextField, InputAdornment } from '@mui/material';
|
||||
import { Radio, Checkbox } from 'components/Form/Settings';
|
||||
|
||||
import languages from '@/i18n/languages.json';
|
||||
import translationPercentages from '@/i18n/translationPercentages.json';
|
||||
|
||||
const LanguageOptions = () => {
|
||||
const t = useT();
|
||||
@@ -29,6 +30,7 @@ const LanguageOptions = () => {
|
||||
// Convert language code to ISO format for Intl.DisplayNames
|
||||
// e.g., "en_GB" -> "en-GB", "zh_CN" -> "zh-CN"
|
||||
const isoCode = lang.value.replace('_', '-');
|
||||
const percentage = translationPercentages[lang.value]?.percent || 0;
|
||||
|
||||
let translatedName;
|
||||
try {
|
||||
@@ -37,25 +39,31 @@ const LanguageOptions = () => {
|
||||
if (translatedName) {
|
||||
translatedName = translatedName.split(' (')[0];
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Fallback if the code isn't recognized
|
||||
translatedName = nativeName;
|
||||
}
|
||||
|
||||
// Show native name first, then translated name in brackets (greyed and smaller)
|
||||
const displayName = !translatedName || translatedName === nativeName
|
||||
? nativeName
|
||||
: (
|
||||
<>
|
||||
{nativeName}{' '}
|
||||
<span style={{ color: '#999', fontSize: '0.85em' }}>({translatedName})</span>
|
||||
</>
|
||||
);
|
||||
const displayName =
|
||||
!translatedName || translatedName === nativeName ? (
|
||||
<>
|
||||
{nativeName} <span style={{ color: '#999', fontSize: '0.85em' }}>({percentage}%)</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{nativeName}{' '}
|
||||
<span style={{ color: '#999', fontSize: '0.85em' }}>
|
||||
({translatedName} • {percentage}%)
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return {
|
||||
name: displayName,
|
||||
value: lang.value,
|
||||
nativeName,
|
||||
percentage,
|
||||
searchText: `${nativeName} ${translatedName || ''}`.toLowerCase(),
|
||||
};
|
||||
});
|
||||
@@ -68,26 +76,26 @@ const LanguageOptions = () => {
|
||||
const filteredLanguages = useMemo(() => {
|
||||
if (!searchQuery.trim()) return languageOptions;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return languageOptions.filter(lang => lang.searchText.includes(query));
|
||||
return languageOptions.filter((lang) => lang.searchText.includes(query));
|
||||
}, [languageOptions, searchQuery]);
|
||||
|
||||
// Detect system language
|
||||
const systemLanguage = useMemo(() => {
|
||||
const browserLang = navigator.language.replace('-', '_');
|
||||
// Check exact match first, then base language
|
||||
return languages.find(l => l.value === browserLang) ||
|
||||
languages.find(l => l.value.startsWith(browserLang.split('_')[0]));
|
||||
return (
|
||||
languages.find((l) => l.value === browserLang) ||
|
||||
languages.find((l) => l.value.startsWith(browserLang.split('_')[0]))
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Find current language option for display
|
||||
const currentLangOption = languageOptions.find(l => l.value === currentLanguage);
|
||||
const currentLangOption = languageOptions.find((l) => l.value === currentLanguage);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="modalHeader">
|
||||
<span className="mainTitle">
|
||||
{languageTitle}
|
||||
</span>
|
||||
<span className="mainTitle">{languageTitle}</span>
|
||||
<div className="headerActions">
|
||||
<a
|
||||
className="link"
|
||||
@@ -107,7 +115,14 @@ const LanguageOptions = () => {
|
||||
category="other"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
placeholder={t('modals.main.settings.sections.language.search')}
|
||||
value={searchQuery}
|
||||
@@ -140,7 +155,8 @@ const LanguageOptions = () => {
|
||||
/>
|
||||
{currentLangOption && (
|
||||
<div style={{ color: '#888', whiteSpace: 'nowrap' }}>
|
||||
{t('modals.main.settings.sections.language.current')}: <strong style={{ color: 'var(--fg)' }}>{currentLangOption.nativeName}</strong>
|
||||
{t('modals.main.settings.sections.language.current')}:{' '}
|
||||
<strong style={{ color: 'var(--fg)' }}>{currentLangOption.nativeName}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react';
|
||||
import { MdOutlineOpenInNew, MdSearch } from 'react-icons/md';
|
||||
import { TextField, InputAdornment } from '@mui/material';
|
||||
import languages from '@/i18n/languages.json';
|
||||
import translationPercentages from '@/i18n/translationPercentages.json';
|
||||
import { useT, useTranslation } from 'contexts/TranslationContext';
|
||||
import variables from 'config/variables';
|
||||
|
||||
@@ -23,6 +24,7 @@ function ChooseLanguage() {
|
||||
const mappedLanguages = languages.map((lang) => {
|
||||
const nativeName = lang.name;
|
||||
const isoCode = lang.value.replace('_', '-');
|
||||
const percentage = translationPercentages[lang.value]?.percent || 0;
|
||||
|
||||
let translatedName;
|
||||
try {
|
||||
@@ -30,17 +32,21 @@ function ChooseLanguage() {
|
||||
if (translatedName) {
|
||||
translatedName = translatedName.split(' (')[0];
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
translatedName = nativeName;
|
||||
}
|
||||
|
||||
const displayName =
|
||||
!translatedName || translatedName === nativeName ? (
|
||||
nativeName
|
||||
<>
|
||||
{nativeName} <span style={{ color: '#999', fontSize: '0.85em' }}>({percentage}%)</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{nativeName}{' '}
|
||||
<span style={{ color: '#999', fontSize: '0.85em' }}>({translatedName})</span>
|
||||
<span style={{ color: '#999', fontSize: '0.85em' }}>
|
||||
({translatedName} • {percentage}%)
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -48,6 +54,7 @@ function ChooseLanguage() {
|
||||
name: displayName,
|
||||
value: lang.value,
|
||||
nativeName,
|
||||
percentage,
|
||||
searchText: `${nativeName} ${translatedName || ''}`.toLowerCase(),
|
||||
};
|
||||
});
|
||||
@@ -60,14 +67,16 @@ function ChooseLanguage() {
|
||||
const filteredLanguages = useMemo(() => {
|
||||
if (!searchQuery.trim()) return languageOptions;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return languageOptions.filter(lang => lang.searchText.includes(query));
|
||||
return languageOptions.filter((lang) => lang.searchText.includes(query));
|
||||
}, [languageOptions, searchQuery]);
|
||||
|
||||
// Detect system language
|
||||
const systemLanguage = useMemo(() => {
|
||||
const browserLang = navigator.language.replace('-', '_');
|
||||
return languages.find(l => l.value === browserLang) ||
|
||||
languages.find(l => l.value.startsWith(browserLang.split('_')[0]));
|
||||
return (
|
||||
languages.find((l) => l.value === browserLang) ||
|
||||
languages.find((l) => l.value.startsWith(browserLang.split('_')[0]))
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
138
src/i18n/translationPercentages.json
Normal file
138
src/i18n/translationPercentages.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"en_GB": {
|
||||
"percent": 100,
|
||||
"lastChange": "2025-11-01T18:26:28.748698Z"
|
||||
},
|
||||
"de_DE": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:51:15.668198Z"
|
||||
},
|
||||
"en_US": {
|
||||
"percent": 99.4,
|
||||
"lastChange": "2025-11-27T16:00:13.299953Z"
|
||||
},
|
||||
"es": {
|
||||
"percent": 99.8,
|
||||
"lastChange": "2025-12-13T18:00:20.128157Z"
|
||||
},
|
||||
"fr": {
|
||||
"percent": 100,
|
||||
"lastChange": "2026-01-02T11:01:50.048816Z"
|
||||
},
|
||||
"id_ID": {
|
||||
"percent": 99,
|
||||
"lastChange": "2025-11-02T18:51:31.688786Z"
|
||||
},
|
||||
"nl": {
|
||||
"percent": 99.4,
|
||||
"lastChange": "2025-11-02T18:51:35.270456Z"
|
||||
},
|
||||
"no": {
|
||||
"percent": 99.4,
|
||||
"lastChange": "2025-11-02T18:51:39.134759Z"
|
||||
},
|
||||
"ru": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:51:43.544130Z"
|
||||
},
|
||||
"tr_TR": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:51:47.582567Z"
|
||||
},
|
||||
"zh_CN": {
|
||||
"percent": 99.2,
|
||||
"lastChange": "2025-11-02T18:51:51.182076Z"
|
||||
},
|
||||
"pt_BR": {
|
||||
"percent": 98.8,
|
||||
"lastChange": "2025-11-02T18:51:54.789336Z"
|
||||
},
|
||||
"es_419": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:51:58.826381Z"
|
||||
},
|
||||
"pt_PT": {
|
||||
"percent": 99,
|
||||
"lastChange": "2025-11-02T18:52:02.248380Z"
|
||||
},
|
||||
"bn": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:06.416365Z"
|
||||
},
|
||||
"sl": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:09.801136Z"
|
||||
},
|
||||
"fa": {
|
||||
"percent": 99.4,
|
||||
"lastChange": "2026-01-03T18:06:23.197313Z"
|
||||
},
|
||||
"az": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:16.631811Z"
|
||||
},
|
||||
"azb": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:20.051219Z"
|
||||
},
|
||||
"zh_Hant": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:23.571948Z"
|
||||
},
|
||||
"lt": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:26.957231Z"
|
||||
},
|
||||
"lv": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:30.768664Z"
|
||||
},
|
||||
"et": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:34.391330Z"
|
||||
},
|
||||
"vi": {
|
||||
"percent": 0.1,
|
||||
"lastChange": "2025-11-01T18:26:36.276619Z"
|
||||
},
|
||||
"ja": {
|
||||
"percent": 1.3,
|
||||
"lastChange": "2025-11-01T18:26:33.191724Z"
|
||||
},
|
||||
"ta": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:38.046456Z"
|
||||
},
|
||||
"ar": {
|
||||
"percent": 99.6,
|
||||
"lastChange": "2025-11-02T18:52:41.378895Z"
|
||||
},
|
||||
"sv": {
|
||||
"percent": 1.3,
|
||||
"lastChange": "2025-11-01T18:26:35.299601Z"
|
||||
},
|
||||
"el": {
|
||||
"percent": 0.1,
|
||||
"lastChange": "2025-11-01T18:26:30.693625Z"
|
||||
},
|
||||
"hu": {
|
||||
"percent": 0,
|
||||
"lastChange": "2025-11-01T18:26:32.407284Z"
|
||||
},
|
||||
"arz": {
|
||||
"percent": 0.1,
|
||||
"lastChange": "2025-11-01T18:26:29.272472Z"
|
||||
},
|
||||
"uk": {
|
||||
"percent": 100,
|
||||
"lastChange": "2025-11-02T18:52:42.303925Z"
|
||||
},
|
||||
"peo": {
|
||||
"percent": 3.7,
|
||||
"lastChange": "2025-12-29T10:00:22.520132Z"
|
||||
},
|
||||
"ko": {
|
||||
"percent": 0,
|
||||
"lastChange": "2026-01-14T23:43:45.065715Z"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user