diff --git a/package-lock.json b/package-lock.json
index 726db1d..af9d51a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -58,7 +58,6 @@
"electron": "^21.3.1",
"electron-builder": "^23.6.0",
"i18next": "^22.4.6",
- "i18next-browser-languagedetector": "^7.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^12.1.1",
@@ -2785,15 +2784,6 @@
"@babel/runtime": "^7.20.6"
}
},
- "node_modules/i18next-browser-languagedetector": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz",
- "integrity": "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==",
- "dev": true,
- "dependencies": {
- "@babel/runtime": "^7.19.4"
- }
- },
"node_modules/iconv-corefoundation": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
@@ -6892,15 +6882,6 @@
"@babel/runtime": "^7.20.6"
}
},
- "i18next-browser-languagedetector": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz",
- "integrity": "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==",
- "dev": true,
- "requires": {
- "@babel/runtime": "^7.19.4"
- }
- },
"iconv-corefoundation": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
diff --git a/package.json b/package.json
index 7f647cc..24edd61 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,6 @@
"electron": "^21.3.1",
"electron-builder": "^23.6.0",
"i18next": "^22.4.6",
- "i18next-browser-languagedetector": "^7.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^12.1.1",
diff --git a/src/app/browser/add-account-manual/index.tsx b/src/app/browser/add-account-manual/index.tsx
index 94cbaec..3f9822b 100644
--- a/src/app/browser/add-account-manual/index.tsx
+++ b/src/app/browser/add-account-manual/index.tsx
@@ -53,7 +53,7 @@ function AddAccountManualPrompt(props: AddAccountManualPromptProps & {
{t('response_heading')}
- {t('response_help_1', {url: 'npf{props.client_id}://auth'})}
+ {t('response_help_1', {url: `npf${props.client_id}://auth`})}
{t('response_help_2')}
'friendcode' in props ? props.friendcode : {
+ id: props.id,
+ regenerable: false,
+ regenerableAt: 0,
+ }, ['friendcode' in props ? props.friendcode : null, 'id' in props ? props.id : null]);
+
+ const onFriendCodeContextMenu = useCallback(() => {
+ ipc.showFriendCodeMenu(friendcode);
+ }, [ipc, friendcode]);
+
+ return SW-{friendcode.id};
+}
+
+const styles = StyleSheet.create({
+ friendCodeValue: {
+ // @ts-expect-error
+ userSelect: 'all',
+ },
+});
diff --git a/src/app/browser/components/icons/add-outline.tsx b/src/app/browser/components/icons/add-outline.tsx
index 4f57c9a..27fe3e6 100644
--- a/src/app/browser/components/icons/add-outline.tsx
+++ b/src/app/browser/components/icons/add-outline.tsx
@@ -2,8 +2,13 @@ import React from 'react';
import { Platform, Text } from 'react-native';
import { svg_styles } from './util.js';
-const IconWeb = React.memo(() =>
-
+const IconWeb = React.memo((props: {
+ title?: string;
+}) =>
+
);
export default Platform.OS === 'web' ? IconWeb : React.memo(() => null);
diff --git a/src/app/browser/components/icons/warning.tsx b/src/app/browser/components/icons/warning.tsx
index d1a2e15..91bba07 100644
--- a/src/app/browser/components/icons/warning.tsx
+++ b/src/app/browser/components/icons/warning.tsx
@@ -2,8 +2,13 @@ import React from 'react';
import { Platform, Text } from 'react-native';
import { svg_styles } from './util.js';
-const IconWeb = React.memo(() =>
-
+const IconWeb = React.memo((props: {
+ title?: string;
+}) =>
+
);
export default Platform.OS === 'web' ? IconWeb : React.memo(() => null);
diff --git a/src/app/browser/components/index.ts b/src/app/browser/components/index.ts
index 5ac39b4..05c5e36 100644
--- a/src/app/browser/components/index.ts
+++ b/src/app/browser/components/index.ts
@@ -1,3 +1,4 @@
export { default as Button } from './button.js';
export { default as NintendoSwitchUser } from './nintendo-switch-user.js';
export { NintendoSwitchUsers } from './nintendo-switch-user.js';
+export { default as FriendCode } from './friend-code.js';
diff --git a/src/app/browser/friend/index.tsx b/src/app/browser/friend/index.tsx
index 6b9f928..00b8262 100644
--- a/src/app/browser/friend/index.tsx
+++ b/src/app/browser/friend/index.tsx
@@ -48,7 +48,7 @@ export default function FriendWindow(props: FriendProps) {
if (!user || !friend || discord_presence_source_state !== RequestState.LOADED) {
return
@@ -62,7 +62,7 @@ export default function FriendWindow(props: FriendProps) {
const can_see_user_presence = user.nsoAccount.user.permissions.presence === PresencePermissions.FRIENDS ||
(user.nsoAccount.user.permissions.presence === PresencePermissions.FAVORITE_FRIENDS && friend.isFavoriteFriend);
- return
+ return
{props.game.name}
{props.game.sysDescription ? {props.game.sysDescription} : null}
- {t('game_played_for', {duration: hrduration(props.game.totalPlayTime)})}
+ {props.game.totalPlayTime >= 60 ?
+ props.game.totalPlayTime % 60 ?
+ t('game_played_for_hm', {hours: Math.floor(props.game.totalPlayTime / 60),
+ minutes: props.game.totalPlayTime % 60}) :
+ t('game_played_for_h', {hours: props.game.totalPlayTime / 60}) :
+ t('game_played_for_m', {minutes: props.game.totalPlayTime})}
{first_played ? t('game_first_played', {
diff --git a/src/app/browser/main/friends.tsx b/src/app/browser/main/friends.tsx
index 5d5b258..9d84d20 100644
--- a/src/app/browser/main/friends.tsx
+++ b/src/app/browser/main/friends.tsx
@@ -1,12 +1,13 @@
import React, { useCallback } from 'react';
import { Image, ImageStyle, Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
-import { useTranslation } from 'react-i18next';
+import { Trans, useTranslation } from 'react-i18next';
import ipc from '../ipc.js';
import { useAccentColour, useColourScheme, User, useTimeSince } from '../util.js';
import { Friend, Presence, PresenceState } from '../../../api/coral-types.js';
import { TEXT_COLOUR_ACTIVE, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
import Section, { HEADER_SIZE } from './section.js';
import AddOutline from '../components/icons/add-outline.js';
+import { FriendCode } from '../components/index.js';
export default function Friends(props: {
user: User;
@@ -23,19 +24,9 @@ export default function Friends(props: {
}, [props.user.user.id]);
const header_buttons =
-
+
;
- const onFriendCodeContextMenu = useCallback(() => {
- ipc.showFriendCodeMenu(props.user.nso!.nsoAccount.user.links.friendCode);
- }, [ipc, props.user.nso?.nsoAccount.user.links.friendCode]);
-
- const fc = SW-{props.user.nso!.nsoAccount.user.links.friendCode.id};
-
return
{props.friends.length ?
@@ -43,11 +34,19 @@ export default function Friends(props: {
:
{t('no_friends')}
- {t('friend_code')}: {fc}
+
+
+
+
+
}
{props.friends.length ?
- {t('friend_code')}: {fc}
+
+
+
+
+
: null}
;
}
@@ -99,7 +98,7 @@ function FriendPresence(props: {
const { t, i18n } = useTranslation('main_window', { keyPrefix: 'friends_section' });
const logout = props.presence.logoutAt ? new Date(props.presence.logoutAt * 1000) : null;
- const since_logout = useTimeSince(logout ?? new Date(0), true);
+ const since_logout = useTimeSince(logout ?? new Date(0), true, i18n.getFixedT(null, 'time_since'));
if (props.presence.state === PresenceState.ONLINE || props.presence.state === PresenceState.PLAYING) {
return {t('presence_playing')};
@@ -124,10 +123,6 @@ const styles = StyleSheet.create({
fontSize: 12,
opacity: 0.7,
},
- friendCodeValue: {
- // @ts-expect-error
- userSelect: 'all',
- },
content: {
paddingBottom: 16,
paddingLeft: ipc.platform === 'win32' ? 24 : 20,
diff --git a/src/app/browser/main/index.tsx b/src/app/browser/main/index.tsx
index 7ddd8c4..097ba70 100644
--- a/src/app/browser/main/index.tsx
+++ b/src/app/browser/main/index.tsx
@@ -36,7 +36,7 @@ export default function App(props: AppProps) {
titleUser={selectedUser}
vibrancy={props.vibrancy}
style={[styles.app, !props.vibrancy ? theme.appNoVibrancy : null]}
- i18nNamespace="main_window"
+ i18nNamespace={['main_window', 'time_since']}
>
) {
const theme = useColourScheme() === 'light' ? light : dark;
const accent_colour = useAccentColour();
+ const { t, i18n } = useTranslation('main_window', { keyPrefix: 'main_section' });
const showErrorDetails = useCallback(() => {
alert(props.error);
@@ -24,7 +26,7 @@ export default function Section(props: React.PropsWithChildren<{
{props.loading ? :
props.error ?
-
+
: null}
{props.headerButtons}
diff --git a/src/app/browser/util.tsx b/src/app/browser/util.tsx
index c5dee59..d5c70e5 100644
--- a/src/app/browser/util.tsx
+++ b/src/app/browser/util.tsx
@@ -1,9 +1,8 @@
import { EventEmitter } from 'node:events';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
-import { ColorSchemeName, I18nManager, LayoutChangeEvent, Platform, StyleProp, StyleSheet, useColorScheme, View, ViewStyle } from 'react-native';
-import { i18n } from 'i18next';
+import { ColorSchemeName, LayoutChangeEvent, Platform, StyleProp, StyleSheet, useColorScheme, View, ViewStyle } from 'react-native';
+import { i18n, TFunction } from 'i18next';
import { I18nextProvider, initReactI18next } from 'react-i18next';
-import LanguageDetector from 'i18next-browser-languagedetector';
import type { User as DiscordUser } from 'discord-rpc';
import { ErrorResponse } from '../../api/util.js';
import { DiscordPresence } from '../../discord/types.js';
@@ -49,10 +48,11 @@ export function Root(props: React.PropsWithChildren<{
// @ts-expect-error
window.i18n = i18n;
- await i18n
- .use(LanguageDetector as unknown as typeof import('i18next-browser-languagedetector').default)
- .use(initReactI18next)
- .init();
+ i18n.use(initReactI18next);
+
+ await i18n.init({
+ lng: ipc.getLanguage(),
+ });
await i18n.loadNamespaces('app');
if (props.i18nNamespace) await i18n.loadNamespaces(props.i18nNamespace);
@@ -305,10 +305,10 @@ export function useActiveDiscordUser() {
return user;
}
-export function useTimeSince(time: Date, short = false) {
+export function useTimeSince(time: Date, short = false, t?: TFunction) {
const [now, setNow] = useState(Date.now());
- const [since, update_in] = getTimeSince(time, now, short ? short_time_since_intervals : time_since_intervals);
+ const [since, update_in] = getTimeSince(time, now, short ? short_time_since_intervals : time_since_intervals, t);
const update_at = Date.now() + update_in;
useEffect(() => {
@@ -323,24 +323,28 @@ interface TimeSinceInterval {
interval: number;
max: number;
string: (count: number) => string;
+ key?: string;
}
const time_since_intervals: TimeSinceInterval[] = [
- {interval: 1000, max: 10, string: () => 'just now'},
- {interval: 1000, max: 60, string: c => c + ' second' + (c === 1 ? '' : 's') + ' ago'},
- {interval: 60 * 1000, max: 60, string: c => c + ' minute' + (c === 1 ? '' : 's') + ' ago'},
- {interval: 60 * 60 * 1000, max: 24, string: c => c + ' hour' + (c === 1 ? '' : 's') + ' ago'},
- {interval: 24 * 60 * 60 * 1000, max: Infinity, string: c => c + ' day' + (c === 1 ? '' : 's') + ' ago'},
+ {interval: 1000, max: 10, string: () => 'just now', key: 'default.now'},
+ {interval: 1000, max: 60, string: c => c + ' second' + (c === 1 ? '' : 's') + ' ago', key: 'default.seconds'},
+ {interval: 60 * 1000, max: 60, string: c => c + ' minute' + (c === 1 ? '' : 's') + ' ago', key: 'default.minutes'},
+ {interval: 60 * 60 * 1000, max: 24, string: c => c + ' hour' + (c === 1 ? '' : 's') + ' ago', key: 'default.hours'},
+ {interval: 24 * 60 * 60 * 1000, max: Infinity, string: c => c + ' day' + (c === 1 ? '' : 's') + ' ago', key: 'default.days'},
];
const short_time_since_intervals: TimeSinceInterval[] = [
- {interval: 1000, max: 10, string: () => 'Just now'},
- {interval: 1000, max: 60, string: c => c + ' sec' + (c === 1 ? '' : 's')},
- {interval: 60 * 1000, max: 60, string: c => c + ' min' + (c === 1 ? '' : 's')},
- {interval: 60 * 60 * 1000, max: 24, string: c => c + ' hr' + (c === 1 ? '' : 's')},
- {interval: 24 * 60 * 60 * 1000, max: Infinity, string: c => c + ' day' + (c === 1 ? '' : 's')},
+ {interval: 1000, max: 10, string: () => 'Just now', key: 'default.now'},
+ {interval: 1000, max: 60, string: c => c + ' sec' + (c === 1 ? '' : 's'), key: 'short.seconds'},
+ {interval: 60 * 1000, max: 60, string: c => c + ' min' + (c === 1 ? '' : 's'), key: 'short.minutes'},
+ {interval: 60 * 60 * 1000, max: 24, string: c => c + ' hr' + (c === 1 ? '' : 's'), key: 'short.hours'},
+ {interval: 24 * 60 * 60 * 1000, max: Infinity, string: c => c + ' day' + (c === 1 ? '' : 's'), key: 'short.days'},
];
-function getTimeSince(time: Date | number, now = Date.now(), intervals = time_since_intervals): [string, number] {
+function getTimeSince(
+ time: Date | number, now = Date.now(),
+ intervals = time_since_intervals, t?: TFunction,
+): [string, number] {
if (time instanceof Date) time = time.getTime();
const elapsed = Math.max(0, now - time);
@@ -349,7 +353,10 @@ function getTimeSince(time: Date | number, now = Date.now(), intervals = time_si
for (const i of intervals) {
if (elapsed < i.max * i.interval || last === i) {
const count = Math.floor(elapsed / i.interval);
- return [i.string.call(null, count), i.interval - (elapsed - (count * i.interval))];
+ return [
+ (t && i.key ? t(i.key, {count, defaultValue: ''}) : '') || i.string.call(null, count),
+ i.interval - (elapsed - (count * i.interval)),
+ ];
}
}
diff --git a/src/app/i18n/index.ts b/src/app/i18n/index.ts
index cfff1a5..b7fde45 100644
--- a/src/app/i18n/index.ts
+++ b/src/app/i18n/index.ts
@@ -3,12 +3,35 @@ import { BackendModule, CallbackError, createInstance, ReadCallback } from 'i18n
const debug = createDebug('app:i18n');
+import './locale/en-gb.js';
+
export const languages = {
- 'en-GB': [() => import('./locale/en-gb.js'), 'English'] as const,
- 'de-DE': [() => import('./locale/de-de.js'), 'Deutsch'] as const,
+ 'en-GB': {
+ name: 'English',
+ app: () => import('./locale/en-gb.js'),
+ authors: [
+ ['Samuel Elliott', 'https://gitlab.fancy.org.uk/samuel', 'https://github.com/samuelthomas2774'],
+ ],
+ },
};
-type Namespace = keyof typeof import('./locale/en-gb.js');
+const namespaces = {
+ app: 'app',
+ app_menu: 'app',
+ menu_app: 'app',
+ menus: 'app',
+ notifications: 'app',
+ handle_uri: 'app',
+
+ main_window: 'app',
+ preferences_window: 'app',
+ friend_window: 'app',
+ addfriend_window: 'app',
+ discordsetup_window: 'app',
+ addaccountmanual_window: 'app',
+} as const;
+
+type Namespace = keyof typeof namespaces;
export default function createI18n() {
const i18n = createInstance({
@@ -41,7 +64,7 @@ const LanguageBackend: BackendModule = {
) => {
debug('Loading %s translations for %s', namespace, language);
- importLocale(language).then(resources => {
+ importLocale(language, namespaces[namespace]).then(resources => {
callback(null, resources[namespace as keyof typeof resources]);
}, (error: CallbackError) => {
callback(error, null);
@@ -50,8 +73,11 @@ const LanguageBackend: BackendModule = {
init: null as any,
};
-async function importLocale(language: keyof typeof languages) {
+async function importLocale(
+ language: keyof typeof languages,
+ chunk: typeof namespaces[keyof typeof namespaces] = 'app',
+) {
if (!(language in languages)) throw new Error('Unknown language ' + language);
- return languages[language][0]();
+ return languages[language][chunk]();
}
diff --git a/src/app/i18n/locale/en-gb.ts b/src/app/i18n/locale/en-gb.ts
index eddf568..869ad3c 100644
--- a/src/app/i18n/locale/en-gb.ts
+++ b/src/app/i18n/locale/en-gb.ts
@@ -5,6 +5,7 @@ export const app = {
licence: LICENCE_NOTICE,
credits: CREDITS_NOTICE,
+ translation_credits: '{{language}} translation by {{authors, list}}.',
};
export const app_menu = {
@@ -68,7 +69,15 @@ export const menus = {
friend: {
presence_online: 'Online',
game_first_played: 'First played: {{date, datetime}}',
- game_play_time: 'Play time: {{time, datetime}}',
+
+ game_play_time_h: 'Play time: $t(friend.hours, {"count": {{hours}}})',
+ game_play_time_hm: 'Play time: $t(friend.hours, {"count": {{hours}}}), $t(friend.minutes, {"count": {{minutes}}})',
+ game_play_time_m: 'Play time: $t(friend.minutes, {"count": {{minutes}}})',
+ hours_one: '{{count}} hour',
+ hours_other: '{{count}} hours',
+ minutes_one: '{{count}} minute',
+ minutes_other: '{{count}} minutes',
+
presence_inactive: 'Offline (console online)',
presence_offline: 'Offline',
presence_updated: 'Updated: {{date, datetime}}',
@@ -90,6 +99,32 @@ export const handle_uri = {
cancel: 'Cancel',
};
+export const time_since = {
+ default: {
+ now: 'just now',
+ seconds_one: '{{count}} second ago',
+ seconds_other: '{{count}} seconds ago',
+ minutes_one: '{{count}} minute ago',
+ minutes_other: '{{count}} minutes ago',
+ hours_one: '{{count}} hour ago',
+ hours_other: '{{count}} hours ago',
+ days_one: '{{count}} day ago',
+ days_other: '{{count}} days ago',
+ },
+
+ short: {
+ now: 'Just now',
+ seconds_one: '{{count}} sec',
+ seconds_other: '{{count}} secs',
+ minutes_one: '{{count}} min',
+ minutes_other: '{{count}} mins',
+ hours_one: '{{count}} hr',
+ hours_other: '{{count}} hrs',
+ days_one: '{{count}} day',
+ days_other: '{{count}} days',
+ },
+};
+
export const main_window = {
sidebar: {
discord_active: 'Discord Rich Presence active',
@@ -127,6 +162,8 @@ export const main_window = {
desc_2: 'Login to the Nintendo Switch Online app to view details here, or use the nxapi command to access Parental Controls data.',
login: 'Login',
},
+
+ section_error: 'Error updating data',
},
discord_section: {
@@ -145,9 +182,10 @@ export const main_window = {
friends_section: {
title: 'Friends',
+ add: 'Add',
no_friends: 'Add friends using a Nintendo Switch console.',
- friend_code: 'Your friend code',
+ friend_code: 'Your friend code: <0>0>',
presence_playing: 'Playing',
presence_offline: 'Offline',
@@ -240,7 +278,14 @@ export const friend_window = {
presence_offline: 'Offline',
presence_last_seen: 'Last seen {{since_logout}}',
- game_played_for: 'Played for {{duration}}',
+ game_played_for_h: 'Played for $t(hours, {"count": {{hours}}})',
+ game_played_for_hm: 'Played for $t(hours, {"count": {{hours}}}), $t(minutes, {"count": {{minutes}}})',
+ game_played_for_m: 'Played for $t(minutes, {"count": {{minutes}}})',
+ hours_one: '{{count}} hour',
+ hours_other: '{{count}} hours',
+ minutes_one: '{{count}} minute',
+ minutes_other: '{{count}} minutes',
+
game_first_played: 'First played {{date, datetime}}',
game_first_played_now: 'First played now',
game_title_id: 'Title ID',
diff --git a/src/app/main/index.ts b/src/app/main/index.ts
index b0c2ec5..612a482 100644
--- a/src/app/main/index.ts
+++ b/src/app/main/index.ts
@@ -21,7 +21,7 @@ import { askUserForUri } from './util.js';
import { setAppInstance, updateMenuLanguage } from './app-menu.js';
import { handleAuthUri } from './na-auth.js';
import { CREDITS_NOTICE, GITLAB_URL, LICENCE_NOTICE } from '../../common/constants.js';
-import createI18n from '../i18n/index.js';
+import createI18n, { languages } from '../i18n/index.js';
const debug = createDebug('app:main');
@@ -131,11 +131,13 @@ export class App {
}
static detectSystemLanguage() {
- const languages = app.getPreferredSystemLanguages().map(l => l.toLowerCase());
+ const preferred = app.getPreferredSystemLanguages().map(l => l.toLowerCase());
const supported = Object.keys(languages).map(l => l.toLowerCase());
- for (const language of languages) {
- if (supported.some(l => language.startsWith(l) || l.startsWith(language))) return language;
+ debug('prefers %O, supports %O', preferred, supported);
+
+ for (const language of preferred) {
+ if (supported.some(l => language.startsWith(l) || l.startsWith(language))) return language;
}
return null;
@@ -143,6 +145,8 @@ export class App {
}
function setAboutPanelOptions(i18n?: i18n) {
+ const language = i18n ? languages[i18n.resolvedLanguage as keyof typeof languages] : undefined;
+
app.setAboutPanelOptions({
applicationName: 'nxapi-app',
applicationVersion: process.platform === 'darwin' ? version : version +
@@ -150,7 +154,10 @@ function setAboutPanelOptions(i18n?: i18n) {
version: git?.revision.substr(0, 8) ?? '?',
authors: ['Samuel Elliott'],
website: GITLAB_URL,
- credits: i18n?.t('app:credits') ?? CREDITS_NOTICE,
+ credits: (i18n?.t('app:credits') ?? CREDITS_NOTICE) +
+ (language?.authors.length ? '\n\n' + i18n!.t('app:translation_credits', {
+ language: language.name, authors: language.authors.map(a => a[0]),
+ }) : ''),
copyright: i18n?.t('app:licence') ?? LICENCE_NOTICE,
});
}
diff --git a/src/app/main/ipc.ts b/src/app/main/ipc.ts
index 1135e1c..bb9e594 100644
--- a/src/app/main/ipc.ts
+++ b/src/app/main/ipc.ts
@@ -28,6 +28,7 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
const t = appinstance.i18n.getFixedT(null, 'menus');
ipcMain.on('nxapi:browser:getwindowdata', e => e.returnValue = getWindowConfiguration(e.sender));
+ ipcMain.on('nxapi:app:language', e => e.returnValue = appinstance.i18n.resolvedLanguage ?? appinstance.i18n.language);
let accent_colour = systemPreferences.getAccentColor?.() || undefined;
@@ -315,9 +316,12 @@ function buildFriendMenu(app: App, user: NintendoAccountUser, nso: CurrentUser,
date: new Date(friend.presence.game.firstPlayedAt * 1000),
formatParams: { date: { dateStyle: 'short', timeStyle: 'medium' } },
})!, enabled: false}),
- new MenuItem({label: t('game_play_time', {
- time: hrduration(friend.presence.game.totalPlayTime),
- })!, enabled: false}),
+ new MenuItem({label: friend.presence.game.totalPlayTime >= 60 ?
+ friend.presence.game.totalPlayTime % 60 ?
+ t('game_play_time_hm', {hours: Math.floor(friend.presence.game.totalPlayTime / 60),
+ minutes: friend.presence.game.totalPlayTime % 60})! :
+ t('game_play_time_h', {hours: friend.presence.game.totalPlayTime / 60})! :
+ t('game_play_time_m', {minutes: friend.presence.game.totalPlayTime})!, enabled: false}),
] : []),
new MenuItem({label: t('presence_updated', {
date: new Date(friend.presence.updatedAt * 1000),
diff --git a/src/app/main/menu.ts b/src/app/main/menu.ts
index 4fd7ec6..4d5a8c0 100644
--- a/src/app/main/menu.ts
+++ b/src/app/main/menu.ts
@@ -14,6 +14,7 @@ import { createWindow } from './windows.js';
import { WindowType } from '../common/types.js';
import CoralApi from '../../api/coral.js';
import { CachedWebServicesList } from '../../common/users.js';
+import { languages } from '../i18n/index.js';
const debug = createDebug('app:main:menu');
@@ -120,6 +121,14 @@ export default class MenuApp {
menu.append(new MenuItem({type: 'separator'}));
menu.append(new MenuItem({label: t('show_main_window')!, click: () => this.app.showMainWindow()}));
menu.append(new MenuItem({label: t('preferences')!, click: () => this.app.showPreferencesWindow()}));
+ if (dev) menu.append(new MenuItem({label: 'Language', submenu: Menu.buildFromTemplate([
+ ...this.app.i18n.options.supportedLngs || ['cimode'],
+ ].map(l => new MenuItem({
+ label: languages[l as keyof typeof languages]?.name ?? l,
+ type: 'checkbox',
+ checked: (this.app.i18n.resolvedLanguage ?? this.app.i18n.language).toLowerCase() === l.toLowerCase(),
+ click: () => this.app.i18n.changeLanguage(l),
+ })))}));
if (dev) menu.append(new MenuItem({label: 'Dump notifications state', click: () => {
debug('Accounts', this.app.monitors.notifications.accounts);
debug('Friends', this.app.monitors.notifications.onlinefriends);
diff --git a/src/app/preload/index.ts b/src/app/preload/index.ts
index 4f4f82c..accb355 100644
--- a/src/app/preload/index.ts
+++ b/src/app/preload/index.ts
@@ -82,6 +82,7 @@ const ipc = {
registerEventListener: (event: string, listener: (args: any[]) => void) => events.on(event, listener),
removeEventListener: (event: string, listener: (args: any[]) => void) => events.removeListener(event, listener),
+ getLanguage: () => language,
getAccentColour: () => accent_colour,
platform: process.platform,
@@ -89,15 +90,20 @@ const ipc = {
export type NxapiElectronIpc = typeof ipc;
-ipcRenderer.on('nxapi:app:update-language', (e, l: string) => events.emit('update-language', l));
ipcRenderer.on('nxapi:window:refresh', () => events.emit('window:refresh') || location.reload());
ipcRenderer.on('nxapi:accounts:shouldrefresh', () => events.emit('update-nintendo-accounts'));
ipcRenderer.on('nxapi:discord:shouldrefresh', () => events.emit('update-discord-presence-source'));
ipcRenderer.on('nxapi:discord:presence', (e, p: DiscordPresence) => events.emit('update-discord-presence', p));
ipcRenderer.on('nxapi:discord:user', (e, u: User) => events.emit('update-discord-user', u));
+let language: string | undefined = invSync('app:language');
+ipcRenderer.on('nxapi:app:update-language', (event, l: string) => {
+ language = l;
+ events.emit('update-language', l);
+});
+
let accent_colour: string | undefined = invSync('systemPreferences:accent-colour');
-ipcRenderer.on('nxapi:systemPreferences:accent-colour', (event, c) => {
+ipcRenderer.on('nxapi:systemPreferences:accent-colour', (event, c: string) => {
accent_colour = c;
events.emit('systemPreferences:accent-colour', c);
});