mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-24 00:41:12 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-settings authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Input, styled } from '@pezkuwi/react-components';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onChange: (key: string, val: string) => void;
|
||||
original: string;
|
||||
tkey: string;
|
||||
tval: string;
|
||||
}
|
||||
|
||||
function StringInput ({ className = '', onChange, original, tkey, tval }: Props): React.ReactElement<Props> {
|
||||
const _onChange = useCallback(
|
||||
(value: string) => onChange(tkey, value),
|
||||
[onChange, tkey]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<div className='label'>{original}</div>
|
||||
<Input
|
||||
onChange={_onChange}
|
||||
value={tval}
|
||||
withLabel={false}
|
||||
/>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
.label {
|
||||
font-style: italic;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
+div {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(StringInput);
|
||||
@@ -0,0 +1,311 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-settings authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Button, Columar, Dropdown, Progress, Spinner, styled, Toggle } from '@pezkuwi/react-components';
|
||||
import i18n from '@pezkuwi/react-components/i18n';
|
||||
import languageCache from '@pezkuwi/react-components/i18n/cache';
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
import { settings } from '@pezkuwi/ui-settings';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import StringInput from './StringInput.js';
|
||||
|
||||
type ProgressType = [[number, number, number], Record<string, [number, number, number]>];
|
||||
type Strings = Record<string, string>;
|
||||
type StringsMod = Record<string, Strings>;
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Defaults {
|
||||
english: StringsMod;
|
||||
keys: Option[];
|
||||
modules: Option[];
|
||||
}
|
||||
|
||||
const cache = new Map<string, unknown>();
|
||||
|
||||
async function retrieveJson (url: string): Promise<any> {
|
||||
if (cache.has(url)) {
|
||||
return cache.get(url);
|
||||
}
|
||||
|
||||
const json = await fetch(`locales/${url}`)
|
||||
.then((response) => response.json())
|
||||
.catch((e) => console.error(e)) as unknown;
|
||||
|
||||
cache.set(url, json);
|
||||
|
||||
return json || {};
|
||||
}
|
||||
|
||||
async function retrieveEnglish (): Promise<StringsMod> {
|
||||
const paths = await retrieveJson('en/index.json') as string[];
|
||||
const strings: Strings[] = await Promise.all(paths.map((path) => retrieveJson(`en/${path}`) as Promise<Strings>));
|
||||
|
||||
return strings.reduce((language: StringsMod, strings, index): StringsMod => {
|
||||
language[paths[index]] = strings;
|
||||
|
||||
return language;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async function retrieveAll (): Promise<Defaults> {
|
||||
const _keys = await retrieveJson('index.json') as string[];
|
||||
const keys = _keys.filter((lng) => lng !== 'en');
|
||||
const missing = keys.filter((lng) => !languageCache[lng]);
|
||||
const english = await retrieveEnglish();
|
||||
const translations = missing.length
|
||||
? await Promise.all(missing.map((lng) => retrieveJson(`${lng}/translation.json`)))
|
||||
: [];
|
||||
|
||||
// setup the language cache
|
||||
missing.forEach((lng, index): void => {
|
||||
languageCache[lng] = translations[index] as Record<string, string>;
|
||||
});
|
||||
|
||||
// fill in all empty values (useful for download, filling in)
|
||||
keys.forEach((lng): void => {
|
||||
Object.keys(english).forEach((record): void => {
|
||||
Object.keys(english[record]).forEach((key): void => {
|
||||
if (!languageCache[lng][key]) {
|
||||
languageCache[lng][key] = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
english,
|
||||
keys: keys.map((text) => ({ text, value: text })),
|
||||
modules: Object
|
||||
.keys(english)
|
||||
.map((text) => ({ text: text.replace('.json', '').replace('app-', 'page-'), value: text }))
|
||||
.sort((a, b) => a.text.localeCompare(b.text))
|
||||
};
|
||||
}
|
||||
|
||||
function calcProgress (english: StringsMod, language: Strings): ProgressType {
|
||||
const breakdown: Record<string, [number, number, number]> = {};
|
||||
let done = 0;
|
||||
let total = 0;
|
||||
|
||||
Object.keys(english).forEach((record): void => {
|
||||
const mod = english[record];
|
||||
const mtotal = Object.keys(mod).length;
|
||||
let mdone = 0;
|
||||
|
||||
Object.keys(mod).forEach((key): void => {
|
||||
if (language[key]) {
|
||||
mdone++;
|
||||
}
|
||||
});
|
||||
|
||||
done += mdone;
|
||||
total += mtotal;
|
||||
|
||||
breakdown[record] = [mdone, mtotal, 0];
|
||||
});
|
||||
|
||||
return [[done, total, 0], breakdown];
|
||||
}
|
||||
|
||||
function doDownload (strings: Strings, withEmpty: boolean): void {
|
||||
const sanitized = Object.keys(strings).sort().reduce((result: Strings, key): Strings => {
|
||||
const sanitized = strings[key].trim();
|
||||
|
||||
if (sanitized || withEmpty) {
|
||||
result[key] = sanitized;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
FileSaver.saveAs(
|
||||
new Blob([JSON.stringify(sanitized, null, 2)], { type: 'application/json; charset=utf-8' }),
|
||||
'translation.json'
|
||||
);
|
||||
}
|
||||
|
||||
function progressDisplay ([done, total, _]: [number, number, number] = [0, 0, 0]): { done: number; progress: string; total: number } {
|
||||
return {
|
||||
done,
|
||||
progress: (total ? (done * 100 / total) : 100).toFixed(2),
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
function Translate ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [withEmpty, toggleWithEmpty] = useToggle();
|
||||
const [{ english, keys, modules }, setDefaults] = useState<Defaults>({ english: {}, keys: [], modules: [] });
|
||||
const [lng, setLng] = useState<string>('zh');
|
||||
const [[modProgress, allProgress], setProgress] = useState<ProgressType>([[0, 0, 0], {}]);
|
||||
const [record, setRecord] = useState<string>('app-accounts.json');
|
||||
const [strings, setStrings] = useState<Strings | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
retrieveAll().then(setDefaults).catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect((): void => {
|
||||
setStrings(languageCache[lng]);
|
||||
setProgress(calcProgress(english, languageCache[lng]));
|
||||
}, [english, lng]);
|
||||
|
||||
useEffect((): void => {
|
||||
setLng(
|
||||
keys.some(({ value }) => value === settings.i18nLang)
|
||||
? settings.i18nLang
|
||||
: 'zh'
|
||||
);
|
||||
}, [keys]);
|
||||
|
||||
const _setString = useCallback(
|
||||
(key: string, value: string): void => {
|
||||
setStrings((strings: Strings | null): Strings | null =>
|
||||
strings
|
||||
? { ...strings, [key]: value }
|
||||
: null
|
||||
);
|
||||
|
||||
const hasPrevVal = !!languageCache[lng][key];
|
||||
const sanitized = value.trim();
|
||||
|
||||
languageCache[lng][key] = value;
|
||||
|
||||
if (hasPrevVal !== !!sanitized) {
|
||||
const [progress, breakdown] = calcProgress(english, languageCache[lng]);
|
||||
|
||||
setProgress(([counters]): ProgressType => {
|
||||
progress[2] = Math.max(0, progress[0] - counters[0]);
|
||||
|
||||
return [progress, breakdown];
|
||||
});
|
||||
}
|
||||
},
|
||||
[english, lng]
|
||||
);
|
||||
|
||||
const _doApply = useCallback(
|
||||
(): void => {
|
||||
i18n.reloadResources().catch(console.error);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const _onDownload = useCallback(
|
||||
() => doDownload(strings || {}, withEmpty),
|
||||
[strings, withEmpty]
|
||||
);
|
||||
|
||||
if (!keys.length) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledMain className={className}>
|
||||
<header>
|
||||
<Columar>
|
||||
<Columar.Column>
|
||||
<div>
|
||||
<Dropdown
|
||||
isFull
|
||||
label={t('the language to display translations for')}
|
||||
onChange={setLng}
|
||||
options={keys}
|
||||
value={lng}
|
||||
/>
|
||||
{t('{{done}}/{{total}}, {{progress}}% done', { replace: progressDisplay(modProgress) })}
|
||||
</div>
|
||||
<Progress
|
||||
total={modProgress[1]}
|
||||
value={modProgress[0]}
|
||||
/>
|
||||
</Columar.Column>
|
||||
<Columar.Column>
|
||||
<div>
|
||||
<Dropdown
|
||||
isFull
|
||||
label={t('the module to display strings for')}
|
||||
onChange={setRecord}
|
||||
options={modules}
|
||||
value={record}
|
||||
/>
|
||||
{t('{{done}}/{{total}}, {{progress}}% done', { replace: progressDisplay(allProgress[record]) })}
|
||||
</div>
|
||||
<Progress
|
||||
total={allProgress[record]?.[1]}
|
||||
value={allProgress[record]?.[0]}
|
||||
/>
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
</header>
|
||||
<div className='toggleWrapper'>
|
||||
<Toggle
|
||||
label={
|
||||
withEmpty
|
||||
? t('include all empty strings in the generated file')
|
||||
: t('do not include empty strings in the generated file')
|
||||
}
|
||||
onChange={toggleWithEmpty}
|
||||
value={withEmpty}
|
||||
/>
|
||||
</div>
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='sync'
|
||||
label={t('Apply to UI')}
|
||||
onClick={_doApply}
|
||||
/>
|
||||
<Button
|
||||
icon='download'
|
||||
label={t('Generate {{lng}}/translation.json', { replace: { lng } })}
|
||||
onClick={_onDownload}
|
||||
/>
|
||||
</Button.Group>
|
||||
{record && strings && Object.keys(english[record]).map((key, index) =>
|
||||
<StringInput
|
||||
key={index}
|
||||
onChange={_setString}
|
||||
original={english[record][key]}
|
||||
tkey={key}
|
||||
tval={strings[key]}
|
||||
/>
|
||||
)}
|
||||
</StyledMain>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledMain = styled.main`
|
||||
.ui--Column {
|
||||
display: flex;
|
||||
|
||||
> div:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.ui--Progress {
|
||||
margin: 0 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.toggleWrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Translate);
|
||||
Reference in New Issue
Block a user