Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
251cf6d
feat: Adds new i18n package and locale resolver
antsgar Sep 14, 2026
6e4e65c
feat: Adds locale service
antsgar Sep 15, 2026
0f30f58
i18n(weekly-mr): Upgrade translations from crowdin (e1e05408).
Sep 14, 2026
0d9ed10
feat: Adds copy plugin pattern
antsgar Sep 15, 2026
8b3a3a2
feat: Adds experimental feature flag and preferences section
antsgar Sep 15, 2026
dfcd8f5
feat: Adds injector and mobx store
antsgar Sep 15, 2026
a9d6878
feat: Adds boot flow
antsgar Sep 15, 2026
e5c8bff
feat: Adds locale preference
antsgar Sep 15, 2026
c5ccdfd
Revert "i18n(weekly-mr): Upgrade translations from crowdin (e1e05408)."
antsgar Sep 18, 2026
e1bd950
i18n(weekly-mr): Upgrade translations from crowdin (fa494166).
Sep 16, 2026
e38caf0
Revert "i18n(weekly-mr): Upgrade translations from crowdin (fa494166)."
antsgar Sep 21, 2026
bbcc72c
i18n(weekly-mr): Upgrade translations from crowdin (fa494166).
Sep 21, 2026
31d0b9b
feat: Adds language selector and re-renders strings on locale update
antsgar Sep 21, 2026
3fdb59e
feat: Adds error toast when loading language fails
antsgar Sep 21, 2026
faed82a
feat: Localize date time strings
antsgar Sep 22, 2026
7b4245a
feat: Add fixes for specific string re-renders
antsgar Sep 22, 2026
6c9b7c5
feat: Localize dates that are pulled from models
antsgar Sep 22, 2026
2c3f863
feat: Fixes localization for snjs strings
antsgar Sep 23, 2026
f8c7841
feat: Add desktop localization
antsgar Sep 25, 2026
1e5eabe
feat: Add mobile localization
antsgar Sep 25, 2026
4f57424
fix: Fixes strings not re-rendering on locale change
antsgar Sep 25, 2026
f1a4cfb
feat: Fixes toggle and reloading behavior
antsgar Sep 28, 2026
d39611e
feat: Update setting copy
antsgar Sep 28, 2026
dcb1128
feat: Adds spotlight modal
antsgar Oct 5, 2026
ac942cd
feat: Adds internal feature flag
antsgar Oct 5, 2026
e7d53cc
feat: Fix modal styling
antsgar Oct 5, 2026
bf4435d
feat: Updates copy
antsgar Oct 5, 2026
a36bfec
feat: Loads persisted locale before app boot
antsgar Oct 6, 2026
e0ba05d
fix: Avoids redundant desktop locale syncs and restores date-at-time …
antsgar Oct 6, 2026
4d7503d
feat: Improves service code
antsgar Oct 6, 2026
5ebbc84
chore: Adds shared type
antsgar Oct 6, 2026
60591b1
chore: Improves spotlight code
antsgar Oct 6, 2026
d348434
chore: Improves date handling code
antsgar Oct 6, 2026
27f3f44
chore: Small code tweaks
antsgar Oct 6, 2026
ef666e4
chore: Revert unrelated changes
antsgar Oct 6, 2026
adccc76
fix: Fixes regressions
antsgar Oct 6, 2026
ec3cde6
fix: Fixes regressions
antsgar Oct 6, 2026
6cc6eb3
fix: Fixes regressions
antsgar Oct 7, 2026
272cdcb
Revert "i18n(weekly-mr): Upgrade translations from crowdin (fa494166)."
antsgar Oct 7, 2026
2b75140
i18n(weekly-mr): Upgrade translations from crowdin (0927732f).
Oct 5, 2026
2908a76
chore: Code and toggle animation improvements
antsgar Oct 7, 2026
59a5397
chore: Add controller tests
antsgar Oct 7, 2026
75e824c
Merge branch 'main' into feat/localization
antsgar Oct 7, 2026
037b905
chore: fixes eslint errors
antsgar Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ packages/responses/dist
packages/sncrypto-common/dist
packages/sncrypto-web/dist
packages/snjs/dist
packages/i18n/dist

**/.pnp.*
**/.yarn/*
Expand Down
4 changes: 4 additions & 0 deletions packages/.locale-state.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"project": "fe-standardnotes",
"locale": "ac161306578fcd17ec1c93f67b26c6d4516d4232"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { addLocale, LocaleData, setDefaultLang, useLocale } from 'ttag'
import fs from 'fs/promises'
import path from 'path'
import { NativeLocalizationState } from '@standardnotes/snjs'
import { reinitializeStrings } from '../Strings'

const DEFAULT_LOCALE = 'en_US'

const loadedLocales = new Set<string>()
let appliedLocale = DEFAULT_LOCALE
let onStringsReloaded: (() => void) | undefined

setDefaultLang(DEFAULT_LOCALE)

export function registerMainProcessLocalizationCallbacks(callbacks: { onStringsReloaded: () => void }): void {
onStringsReloaded = callbacks.onStringsReloaded
}

const WEB_LOCALE_FILE_PATTERN = /^(config\/locales|[a-z]{2,3}_[A-Za-z0-9]{2,4})\.json$/

function getLocalesDirectory(): string {
return path.join(__dirname, 'locales')
}

export async function readWebLocaleFile(relativePath: string): Promise<string> {
if (!WEB_LOCALE_FILE_PATTERN.test(relativePath)) {
throw new Error(`Invalid web locale file: ${relativePath}`)
}

return fs.readFile(path.join(__dirname, 'web', 'locales', relativePath), 'utf8')
}

async function activateLocale(locale: string): Promise<void> {
if (locale !== DEFAULT_LOCALE && !loadedLocales.has(locale)) {
const filePath = path.join(getLocalesDirectory(), `${locale}.json`)
const contents = await fs.readFile(filePath, 'utf8')
addLocale(locale, JSON.parse(contents) as LocaleData)
loadedLocales.add(locale)
}

useLocale(locale)
}

export async function syncMainProcessLocalization(state: NativeLocalizationState): Promise<void> {
const locale = state.localizationEnabled && state.locale ? state.locale : DEFAULT_LOCALE
if (locale === appliedLocale) {
return
}

try {
await activateLocale(locale)
appliedLocale = locale
} catch (error) {
console.error(`Failed to load desktop locale pack for ${locale}`, error)
useLocale(DEFAULT_LOCALE)
appliedLocale = DEFAULT_LOCALE
}

reinitializeStrings()
onStringsReloaded?.()
}
2 changes: 1 addition & 1 deletion packages/desktop/app/javascripts/Main/Menus/Menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ function windowMenu(store: Store, trayManager: TrayManager, reload: () => void):
role: Roles.Window,
submenu: [
menuRole(str().minimize, Roles.Minimize),
menuRole(str().close, Roles.Close),
menuRole(isMac() ? str().closeWindow : str().close, Roles.Close),
Separator,
...(isMac() ? macWindowItems() : [minimizeToTrayItem(store, trayManager, reload)]),
],
Expand Down
12 changes: 12 additions & 0 deletions packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { Component, PackageManagerInterface } from '../Packages/PackageManagerIn
import { SearchManagerInterface } from '../Search/SearchManagerInterface'
import { RemoteDataInterface } from './DataInterface'
import { MediaManagerInterface } from '../Media/MediaManagerInterface'
import { readWebLocaleFile, syncMainProcessLocalization } from '../Localization/MainProcessLocalization'
import { NativeLocalizationState } from '@standardnotes/snjs'

/**
* Read https://github.com/electron/remote to understand how electron/remote works.
Expand Down Expand Up @@ -92,6 +94,8 @@ export class RemoteBridge implements CrossProcessBridge {
getHomeServerLogs: this.getHomeServerLogs.bind(this),
getHomeServerUrl: this.getHomeServerUrl.bind(this),
getHomeServerLastErrorMessage: this.getHomeServerLastErrorMessage.bind(this),
syncMainProcessLocalization: this.syncMainProcessLocalization.bind(this),
readWebLocaleFile: this.readWebLocaleFile.bind(this),
}
}

Expand Down Expand Up @@ -302,4 +306,12 @@ export class RemoteBridge implements CrossProcessBridge {
async getHomeServerLastErrorMessage(): Promise<string | undefined> {
return this.homeServerManager.getHomeServerLastErrorMessage()
}

async syncMainProcessLocalization(state: NativeLocalizationState): Promise<void> {
await syncMainProcessLocalization(state)
}

async readWebLocaleFile(relativePath: string): Promise<string> {
return readWebLocaleFile(relativePath)
}
}
5 changes: 5 additions & 0 deletions packages/desktop/app/javascripts/Main/Strings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export function initializeStrings(_locale: string): void {
strings = createStrings()
}

/** Rebuilds menu/tray strings after the active ttag locale changes. */
export function reinitializeStrings(): void {
strings = createStrings()
}

export function str(): Strings {
if (isDev()) {
if (!strings) {
Expand Down
1 change: 1 addition & 0 deletions packages/desktop/app/javascripts/Main/Strings/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function createStrings(): Strings {
clearCacheAndReload: c('B8.MobileDesktopShared.Desktop.Menu.Label').t`Clear Cache and Reload`,
speech: c('B8.MobileDesktopShared.Desktop.Menu.Label').t`Speech`,
close: c('B8.MobileDesktopShared.Desktop.Menu.Label').t`Close`,
closeWindow: c('B8.MobileDesktopShared.Desktop.Menu.Label').t`Close Window`,
minimize: c('B8.MobileDesktopShared.Desktop.Menu.Label').t`Minimize`,
zoom: c('B8.MobileDesktopShared.Desktop.Menu.Label').t`Zoom`,
bringAllToFront: c('B8.MobileDesktopShared.Desktop.Menu.Label').t`Bring All to Front`,
Expand Down
1 change: 1 addition & 0 deletions packages/desktop/app/javascripts/Main/Strings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ interface AppMenuStrings {
clearCacheAndReload: string
speech: string
close: string
closeWindow: string
minimize: string
zoom: string
bringAllToFront: string
Expand Down
17 changes: 17 additions & 0 deletions packages/desktop/app/javascripts/Main/TrayManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export interface TrayManager {
shouldMinimizeToTray(): boolean
createTrayIcon(): void
destroyTrayIcon(): void
reloadLabels(): void
}

export function createTrayManager(window: Electron.BrowserWindow, store: Store): TrayManager {
let tray: Tray | undefined
let updateContextMenu: (() => void) | undefined
let applyTrayLabels: (() => void) | undefined

function showWindow() {
window.show()
Expand Down Expand Up @@ -70,6 +72,15 @@ export function createTrayManager(window: Electron.BrowserWindow, store: Store):
},
])

applyTrayLabels = function applyTrayLabels() {
trayContextMenu.getMenuItemById(SHOW_WINDOW_ID)!.label = str().show
trayContextMenu.getMenuItemById(HIDE_WINDOW_ID)!.label = str().hide
const quitItem = trayContextMenu.items.find((item) => item.role === 'quit')
if (quitItem) {
quitItem.label = str().quit
}
}

updateContextMenu = function updateContextMenu() {
if (window.isVisible()) {
trayContextMenu.getMenuItemById(SHOW_WINDOW_ID)!.visible = false
Expand Down Expand Up @@ -105,6 +116,12 @@ export function createTrayManager(window: Electron.BrowserWindow, store: Store):
tray!.destroy()
tray = undefined
updateContextMenu = undefined
applyTrayLabels = undefined
},

reloadLabels() {
applyTrayLabels?.()
updateContextMenu?.()
},
}
}
8 changes: 8 additions & 0 deletions packages/desktop/app/javascripts/Main/Window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { initializeZoomManager } from './ZoomManager'
import { HomeServerManager } from './HomeServer/HomeServerManager'
import { FilesManager } from './File/FilesManager'
import { DirectoryManager } from './Directory/DirectoryManager'
import { registerMainProcessLocalizationCallbacks } from './Localization/MainProcessLocalization'

const WINDOW_DEFAULT_WIDTH = 1100
const WINDOW_DEFAULT_HEIGHT = 800
Expand Down Expand Up @@ -224,6 +225,13 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
spellcheckerManager,
})

registerMainProcessLocalizationCallbacks({
onStringsReloaded: () => {
menuManager.reload()
trayManager.reloadLabels()
},
})

const fileBackupsManager = new FilesBackupManager(appState, window.webContents, filesManager)

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
HomeServerManagerInterface,
} from '@web/Application/Device/DesktopSnjsExports'
import { Component } from '../Main/Packages/PackageManagerInterface'
import { NativeLocalizationState } from '@standardnotes/snjs'

export interface CrossProcessBridge extends FileBackupsDevice, DirectoryManagerInterface, HomeServerManagerInterface {
get extServerHost(): string
Expand All @@ -25,4 +26,6 @@ export interface CrossProcessBridge extends FileBackupsDevice, DirectoryManagerI
onSearch(text: string): void
destroyAllData(): void
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
syncMainProcessLocalization(state: NativeLocalizationState): Promise<void>
readWebLocaleFile(relativePath: string): Promise<string>
}
4 changes: 4 additions & 0 deletions packages/desktop/desktop.webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ module.exports = function ({ onlyTranspileTypescript = false, experimentalFeatur
from: 'app/icon',
to: 'icon',
},
{
from: 'locales',
to: 'locales',
},
],
}),
],
Expand Down
10 changes: 10 additions & 0 deletions packages/desktop/locales/config/locales.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"zh_CN": "简体中文",
"en_US": "English",
"fr_FR": "Français",
"de_DE": "Deutsch",
"ja_JP": "日本語",
"pt_BR": "Português (Brasil)",
"es_LA": "Español (Latinoamérica)",
"tr_TR": "Türkçe"
}
Loading
Loading