Skip to content

Commit

Permalink
Merge branch 'whitescreen' into release-v4.47.0
Browse files Browse the repository at this point in the history
  • Loading branch information
benma committed Feb 24, 2025
2 parents 55c467d + 0c430b4 commit 9d21e59
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 122 deletions.
12 changes: 0 additions & 12 deletions cmd/servewallet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,6 @@ func (webdevEnvironment) NativeLocale() string {
if v == "" {
v = os.Getenv("LANG")
}
// try macOS specific AppleLocale
if v == "" && runtime.GOOS == "darwin" {
cmd := exec.Command("defaults", "read", "-g", "AppleLocale") // may return something like en_US@rg=chzzzz
out, err := cmd.Output()
if err == nil {
v = strings.Split(string(out), "@")[0]
}
}
// If still empty, provide a default
if v == "" {
v = "en_US" // Default to English (United States)
}
// Strip charset from the LANG. It is unsupported by JS Date formatting
// used in the frontend and breaks UI in unexpected ways.
// We are always UTF-8 anyway.
Expand Down
2 changes: 1 addition & 1 deletion frontends/web/src/components/guide/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const Entry = (props: TProps) => {
<div className={[style.entryContent, shown ? style.expanded : ''].join(' ')}>
{shown ? (
<div className="flex-1">
{entry?.text?.trim().split('\n').map((p, idx) => <p key={idx}>{p}</p>)}
{entry.text.trim().split('\n').map((p, idx) => <p key={idx}>{p}</p>)}
{entry.link && (
<p>
<A
Expand Down
9 changes: 1 addition & 8 deletions frontends/web/src/components/language/language.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,7 @@ import { useTranslation } from 'react-i18next';
import { TLanguagesList } from './types';

vi.mock('react-i18next', () => ({
useTranslation: vi.fn(),
hasResourceBundle: vi.fn(),
addResourceBundle: vi.fn(),
changeLanguage: vi.fn()
}));

vi.mock('@/i18n/i18n', () => ({
changei18nLanguage: vi.fn()
useTranslation: vi.fn()
}));

describe('components/language/language', () => {
Expand Down
7 changes: 3 additions & 4 deletions frontends/web/src/components/language/language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { Dialog } from '@/components/dialog/dialog';
import { defaultLanguages, TActiveLanguageCodes, TLanguagesList } from './types';
import style from './language.module.css';
import { getSelectedIndex } from '@/utils/language';
import { changei18nLanguage } from '@/i18n/i18n';

type TLanguageSwitchProps = {
languages?: TLanguagesList;
Expand All @@ -35,10 +34,10 @@ const LanguageSwitch = ({ languages }: TLanguageSwitchProps) => {
const [selectedIndex, setSelectedIndex] = useState<number>(getSelectedIndex(allLanguages, i18n));
const [activeDialog, setActiveDialog] = useState<boolean>(false);

const changeLanguage = async (langCode: TActiveLanguageCodes, index: number) => {
const changeLanguage = (langCode: TActiveLanguageCodes, index: number) => {
setSelectedIndex(index);
setActiveDialog(false);
await changei18nLanguage(langCode);
i18n.changeLanguage(langCode);
};

if (allLanguages.length === 1) {
Expand Down Expand Up @@ -107,4 +106,4 @@ const LanguageSwitch = ({ languages }: TLanguageSwitchProps) => {
);
};

export { LanguageSwitch };
export { LanguageSwitch };
12 changes: 6 additions & 6 deletions frontends/web/src/i18n/config.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { languageFromConfig } from './config';
describe('language detector', () => {
it('defaults to english', () => new Promise<void>(done => {
(apiGet as Mock).mockResolvedValue({});
languageFromConfig.detect((lang) => {
languageFromConfig.detect((lang: any) => {
expect(lang).toEqual('en');
done();
});
Expand All @@ -43,7 +43,7 @@ describe('language detector', () => {
default: { return Promise.resolve(); }
}
});
languageFromConfig.detect((lang) => {
languageFromConfig.detect((lang: any) => {
expect(lang).toEqual('it');
done();
});
Expand All @@ -57,7 +57,7 @@ describe('language detector', () => {
default: { return Promise.resolve(); }
}
});
languageFromConfig.detect((lang) => {
languageFromConfig.detect((lang: any) => {
expect(lang).toEqual('de');
done();
});
Expand All @@ -71,7 +71,7 @@ describe('language detector', () => {
default: { return Promise.resolve(); }
}
});
languageFromConfig.detect((lang) => {
languageFromConfig.detect((lang: any) => {
expect(lang).toEqual('en');
done();
});
Expand All @@ -85,7 +85,7 @@ describe('language detector', () => {
default: { return Promise.resolve(); }
}
});
languageFromConfig.detect((lang) => {
languageFromConfig.detect((lang: any) => {
expect(lang).toEqual('de');
done();
});
Expand All @@ -99,7 +99,7 @@ describe('language detector', () => {
default: { return Promise.resolve(); }
}
});
languageFromConfig.detect((lang) => {
languageFromConfig.detect((lang: any) => {
expect(lang).toEqual('pt-BR');
done();
});
Expand Down
9 changes: 3 additions & 6 deletions frontends/web/src/i18n/i18n.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ vi.mock('@/utils/request', () => ({
}));

import { apiGet, apiPost } from '@/utils/request';
import { changei18nLanguage } from './i18n';
import { i18n } from './i18n';

describe('i18n', () => {
describe('languageChanged', () => {
Expand All @@ -37,9 +37,6 @@ describe('i18n', () => {
{ nativeLocale: 'de-DE', newLang: 'de', userLang: null },
{ nativeLocale: 'pt_BR', newLang: 'pt', userLang: null },
{ nativeLocale: 'fr', newLang: 'en', userLang: 'en' },
{ nativeLocale: 'WAGA_WAGA', newLang: 'en', userLang: 'en' }, // unknown locale
{ nativeLocale: '', newLang: 'fr', userLang: 'fr' }, // empty locale
{ nativeLocale: '-_-_', newLang: 'de', userLang: 'de' }, // with invalid locale
];
table.forEach((test) => {
it(`sets userLanguage to ${test.userLang || 'null'} if native-locale is ${test.nativeLocale}`, async () => {
Expand All @@ -50,9 +47,9 @@ describe('i18n', () => {
default: { return Promise.resolve(); }
}
});
await changei18nLanguage(test.newLang);
await i18n.changeLanguage(test.newLang);
await waitFor(() => {
expect(apiPost).toHaveBeenCalled();
expect(apiPost).toHaveBeenCalledTimes(1);
expect(apiPost).toHaveBeenCalledWith('config', {
frontend: {},
backend: { userLanguage: test.userLang },
Expand Down
141 changes: 58 additions & 83 deletions frontends/web/src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,60 +17,36 @@

import i18n from 'i18next';
import { getNativeLocale } from '@/api/nativelocale';
import appTranslationsAR from '@/locales/ar/app.json';
import appTranslationsCS from '@/locales/cs/app.json';
import appTranslationsDE from '@/locales/de/app.json';
import appTranslationsEN from '@/locales/en/app.json';
import appTranslationsFR from '@/locales/fr/app.json';
import appTranslationsJA from '@/locales/ja/app.json';
import appTranslationsRU from '@/locales/ru/app.json';
import appTranslationsMS from '@/locales/ms/app.json';
import appTranslationsNL from '@/locales/nl/app.json';
import appTranslationsPT from '@/locales/pt/app.json';
import appTranslationsHI from '@/locales/hi/app.json';
import appTranslationsBG from '@/locales/bg/app.json';
import appTranslationsTR from '@/locales/tr/app.json';
import appTranslationsZH from '@/locales/zh/app.json';
import appTranslationsFA from '@/locales/fa/app.json';
import appTranslationsES from '@/locales/es/app.json';
import appTranslationsSL from '@/locales/sl/app.json';
import appTranslationsHE from '@/locales/he/app.json';
import appTranslationsIT from '@/locales/it/app.json';
import { languageFromConfig } from './config';
import { localeMainLanguage } from './utils';
import { setConfig } from '@/utils/config';
import appTranslationsEN from '@/locales/en/app.json';

const locizeProjectID = 'fe4e5a24-e4a2-4903-96fc-3d62c11fc502';

let isChangingLanguage = false;
const defaultFallbackLang = 'en';

const languageResources = {
ar: () => import('@/locales/ar/app.json'),
cs: () => import('@/locales/cs/app.json'),
de: () => import('@/locales/de/app.json'),
en: () => Promise.resolve({ default: appTranslationsEN }),
fr: () => import('@/locales/fr/app.json'),
ja: () => import('@/locales/ja/app.json'),
ru: () => import('@/locales/ru/app.json'),
ms: () => import('@/locales/ms/app.json'),
nl: () => import('@/locales/nl/app.json'),
pt: () => import('@/locales/pt/app.json'),
hi: () => import('@/locales/hi/app.json'),
bg: () => import('@/locales/bg/app.json'),
tr: () => import('@/locales/tr/app.json'),
zh: () => import('@/locales/zh/app.json'),
fa: () => import('@/locales/fa/app.json'),
es: () => import('@/locales/es/app.json'),
sl: () => import('@/locales/sl/app.json'),
he: () => import('@/locales/he/app.json'),
it: () => import('@/locales/it/app.json')
};

type LanguageKey = keyof typeof languageResources;

export const loadLanguage = async (language: string) => {
try {
const resources = await languageResources[language as LanguageKey]();
if (!i18n.hasResourceBundle(language, 'app')) {
i18n.addResourceBundle(language, 'app', resources.default || resources);
}
} catch (error) {
console.error(`Failed to load language resources for ${language}:`, error);
}
};

export const changei18nLanguage = async (language: string) => {
await loadLanguage(language);
await i18n.changeLanguage(language);
};

let i18Init = i18n.use(languageFromConfig);
let i18Init = i18n
.use(languageFromConfig);

i18Init.init({
fallbackLng: defaultFallbackLang,
fallbackLng: 'en',

// have a common namespace used around the full app
ns: ['app', 'wallet'],
Expand All @@ -83,56 +59,55 @@ i18Init.init({
},

react: {
useSuspense: true // Not using Suspense you will need to handle the not ready state yourself
useSuspense : true, // Not using Suspense you will need to handle the not ready state yourself
},

backend: {
projectId: locizeProjectID,
referenceLng: defaultFallbackLang
}
referenceLng: 'en'
},
});

// always include 'en' so we have a fallback for keys that are not translated
i18n.addResourceBundle(defaultFallbackLang, 'app', appTranslationsEN);

i18n.on('languageChanged', async (lng) => {
// changei18nLanguage triggers languageChanged, thus this check to prevent loop
if (isChangingLanguage) {
return;
}

try {
isChangingLanguage = true;
// Set userLanguage in config back to empty if system locale matches
// the newly selected language lng to make the app use native-locale again.
// This also covers partial matches. For example, if native locale is pt_BR
// and the app has only pt translation, assume they match.
//
// Since userLanguage is stored in the backend config as a string,
// setting it to null here in JS turns it into an empty string "" in Go backend.
// This is ok since we're just checking for a truthy value in the language detector.
const nativeLocale = await getNativeLocale();
i18n.addResourceBundle('ar', 'app', appTranslationsAR);
i18n.addResourceBundle('cs', 'app', appTranslationsCS);
i18n.addResourceBundle('de', 'app', appTranslationsDE);
i18n.addResourceBundle('en', 'app', appTranslationsEN);
i18n.addResourceBundle('fr', 'app', appTranslationsFR);
i18n.addResourceBundle('ja', 'app', appTranslationsJA);
i18n.addResourceBundle('ms', 'app', appTranslationsMS);
i18n.addResourceBundle('nl', 'app', appTranslationsNL);
i18n.addResourceBundle('ru', 'app', appTranslationsRU);
i18n.addResourceBundle('pt', 'app', appTranslationsPT);
i18n.addResourceBundle('hi', 'app', appTranslationsHI);
i18n.addResourceBundle('bg', 'app', appTranslationsBG);
i18n.addResourceBundle('tr', 'app', appTranslationsTR);
i18n.addResourceBundle('zh', 'app', appTranslationsZH);
i18n.addResourceBundle('fa', 'app', appTranslationsFA);
i18n.addResourceBundle('es', 'app', appTranslationsES);
i18n.addResourceBundle('sl', 'app', appTranslationsSL);
i18n.addResourceBundle('he', 'app', appTranslationsHE);
i18n.addResourceBundle('it', 'app', appTranslationsIT);

i18n.on('languageChanged', (lng) => {
// Set userLanguage in config back to empty if system locale matches
// the newly selected language lng to make the app use native-locale again.
// This also covers partial matches. For example, if native locale is pt_BR
// and the app has only pt translation, assume they match.
//
// Since userLanguage is stored in the backend config as a string,
// setting it to null here in JS turns it into an empty string "" in Go backend.
// This is ok since we're just checking for a truthy value in the language detector.
return getNativeLocale().then((nativeLocale) => {
let match = lng === nativeLocale;

if (!match) {
const localeLang = localeMainLanguage(nativeLocale);
// There are too many combinations. So, we compare only the main
// language tag.
const lngLang = localeMainLanguage(lng);
await changei18nLanguage(localeMainLanguage(lng));
const localeLang = localeMainLanguage(nativeLocale);
match = lngLang === localeLang;
}

const uiLang = match ? null : lng;
return setConfig({ backend: { userLanguage: uiLang } });
} finally {
isChangingLanguage = false;
}
});

i18n.on('initialized', () => {
languageFromConfig.detect((lang) => {
if (lang && typeof lang === 'string') {
changei18nLanguage(localeMainLanguage(lang));
}
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { useTranslation } from 'react-i18next';
import { useLoad } from '@/hooks/api';
import { getNativeLocale } from '@/api/nativelocale';
import { defaultLanguages } from '@/components/language/types';
import { changei18nLanguage } from '@/i18n/i18n';
import { Dropdown } from '@/components/dropdown/dropdown';
import { getSelectedIndex } from '@/utils/language';
import { GlobeDark, GlobeLight } from '@/components/icon/icon';
Expand All @@ -45,7 +44,7 @@ export const LanguageDropdownSetting = () => {
className={settingsDropdownStyles.select}
renderOptions={(o) => (o.label)}
options={formattedLanguages}
onChange={(selected) => changei18nLanguage(selected.value)}
onChange={(selected) => i18n.changeLanguage(selected.value)}
value={{ label: selectedLanguage.display, value: selectedLanguage.code }}
/>
}
Expand Down

0 comments on commit 9d21e59

Please sign in to comment.