Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions assets/locales/en.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# English translations for Ropy

# Application
app_name = "Ropy"
app_description = "A clipboard manager built with Rust and GPUI"

# Tray menu
tray_show = "Show"
tray_quit = "Quit"

# Main window
search_placeholder = "Use / to search ... "
no_records = "No clipboard records"
clear_all = "Clear All"
pin = "Pin"
unpin = "Unpin"

# Settings
settings_title = "Ropy Settings"
settings_back = "←"
settings_cancel = "Cancel"
settings_save = "Save"

# Settings sections
settings_language = "Language"
settings_language_description = "Select your preferred language"

settings_theme = "Theme"
settings_theme_light = "Light"
settings_theme_dark = "Dark"
settings_theme_system = "System"

settings_hotkey = "Hotkey Configuration"
settings_activation_key = "Activation Key"

settings_storage = "Storage Configuration"
settings_max_history = "Max History Records"

settings_system = "System"
settings_autostart = "Launch at system startup"
settings_autostart_on = "ON"
settings_autostart_off = "OFF"

# Content types
content_type_text = "Text"
content_type_image = "Image"
content_type_file = "File"

# Messages
message_copied = "Copied to clipboard"
message_deleted = "Record deleted"
message_cleared = "History cleared"
52 changes: 52 additions & 0 deletions assets/locales/zh-CN.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 简体中文翻译

# 应用程序
app_name = "Ropy"
app_description = "基于 Rust 和 GPUI 构建的剪贴板管理器"

# 托盘菜单
tray_show = "显示"
tray_quit = "退出"

# 主窗口
search_placeholder = "使用 / 搜索 ... "
no_records = "无剪贴板记录"
clear_all = "清空全部"
pin = "固定"
unpin = "取消固定"

# 设置
settings_title = "Ropy 设置"
settings_back = "←"
settings_cancel = "取消"
settings_save = "保存"

# 设置选项
settings_language = "语言"
settings_language_description = "选择您偏好的语言"

settings_theme = "主题"
settings_theme_light = "浅色"
settings_theme_dark = "深色"
settings_theme_system = "跟随系统"

settings_hotkey = "快捷键配置"
settings_activation_key = "激活快捷键"

settings_storage = "存储配置"
settings_max_history = "最大历史记录数"

settings_system = "系统"
settings_autostart = "开机自动启动"
settings_autostart_on = "开启"
settings_autostart_off = "关闭"

# 内容类型
content_type_text = "文本"
content_type_image = "图片"
content_type_file = "文件"

# 消息
message_copied = "已复制到剪贴板"
message_deleted = "记录已删除"
message_cleared = "历史已清空"
50 changes: 50 additions & 0 deletions doc/Multi-Language.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Multi-Language Support

Ropy now supports multiple languages! Users can easily switch between different languages through the settings panel.

## Supported Languages

- **English** (en)
- **简体中文** (Simplified Chinese, zh-CN)

## How to Use

1. Open Ropy application
2. Click the **Settings** button (gear icon) in the top right corner
3. In the Settings panel, find the **Language** section
4. Click on your preferred language button
5. Click **Save** to apply the changes
6. The UI will immediately switch to the selected language

## Adding New Languages

To add a new language:

1. Create a new TOML file in `assets/locales/` directory (e.g., `fr.toml` for French)
2. Copy the structure from `en.toml` and translate all strings
3. Update `src/i18n/mod.rs`:
- Add a new variant to the `Language` enum
- Update the `code()`, `display_name()`, and `all()` methods
- Add the new language to the `load_language()` match statement
4. Recompile the application

## Translation File Format

Translation files use TOML format with simple key-value pairs:

```toml
# Application
app_name = "Ropy"
app_description = "A clipboard manager built with Rust and GPUI"

# Tray menu
tray_show = "Show"
tray_quit = "Quit"
```

## Implementation Details

- Translations are embedded into the binary at compile time using `include_str!()` for optimal performance
- The i18n system is lightweight and has minimal runtime overhead
- Language selection is persisted in the user's configuration file
- All UI strings are centralized in locale files for easy maintenance
1 change: 0 additions & 1 deletion doc/TODO.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# TODO

- Import and export clipboard history
- Support for multiple languages
- Support for html content type
- Log files
- Provide installation packages and distribution channels
Expand Down
4 changes: 4 additions & 0 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::i18n::Language;
use config::{Config, ConfigError, File};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
Expand All @@ -13,6 +14,8 @@ pub struct Settings {
pub theme: AppTheme,
/// Auto-start configuration
pub autostart: AutoStartSettings,
/// Language configuration
pub language: Language,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -67,6 +70,7 @@ impl Default for Settings {
},
theme: AppTheme::System,
autostart: AutoStartSettings { enabled: false },
language: Language::default(),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/gui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ pub fn launch_app() {
copy_tx,
);
setup_hotkey_listener(window_handle, async_app.clone());
start_tray_handler(window_handle, async_app.clone());
start_tray_handler(window_handle, async_app.clone(), settings.clone());

cx.activate(true);
});
Expand Down
31 changes: 29 additions & 2 deletions src/gui/board/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod settings;
use crate::clipboard::LastCopyState;
use crate::config::Settings;
use crate::gui::hide_window;
use crate::i18n::{I18n, Language};
use crate::repository::models::ContentType;
use crate::repository::{ClipboardRecord, ClipboardRepository};
use gpui::{
Expand Down Expand Up @@ -41,6 +42,9 @@ pub struct RopyBoard {
selected_theme: usize, // 0: Light, 1: Dark, 2: System
autostart_enabled: bool,
pinned: bool,
// I18n
i18n: I18n,
selected_language: usize, // Index into Language::all()
}

impl RopyBoard {
Expand Down Expand Up @@ -69,7 +73,7 @@ impl RopyBoard {
cx.new(|cx| InputState::new(window, cx).placeholder("Use / to search ... "));
let list_state = ListState::new(0, ListAlignment::Top, gpui::px(100.));

let (max_history_records, activation_key, theme_index) = {
let (max_history_records, activation_key, theme_index, language) = {
let settings_guard = settings.read().unwrap();
let theme_idx = match settings_guard.theme {
crate::config::AppTheme::Light => 0,
Expand All @@ -80,13 +84,21 @@ impl RopyBoard {
settings_guard.storage.max_history_records,
settings_guard.hotkey.activation_key.clone(),
theme_idx,
settings_guard.language,
)
};
let autostart_enabled = settings.read().unwrap().autostart.enabled;
let settings_activation_key_input =
cx.new(|cx| InputState::new(window, cx).placeholder(activation_key.to_string()));
let settings_max_history_input =
cx.new(|cx| InputState::new(window, cx).placeholder(max_history_records.to_string()));

// Initialize I18n with the language from settings
let i18n = I18n::new(language).unwrap_or_default();
let selected_language = Language::all()
.iter()
.position(|&lang| lang == language)
.unwrap_or(0);

Self {
records,
Expand All @@ -106,6 +118,8 @@ impl RopyBoard {
selected_theme: theme_index,
autostart_enabled,
pinned: false,
i18n,
selected_language,
}
}

Expand Down Expand Up @@ -212,17 +226,30 @@ impl RopyBoard {
_ => crate::config::AppTheme::System,
};

let language = Language::all().get(self.selected_language).copied().unwrap_or_default();

{
let mut settings = self.settings.write().unwrap();
settings.hotkey.activation_key = activation_key.clone();
settings.storage.max_history_records = max_history;
settings.theme = theme.clone();
settings.autostart.enabled = self.autostart_enabled;
settings.language = language;
if let Err(e) = settings.save() {
eprintln!("[ropy] Failed to save settings: {e}");
}
}

// Apply the new language
if let Err(e) = self.i18n.set_language(language) {
eprintln!("[ropy] Failed to set language: {e}");
}

// Update search placeholder with new language
self.search_input.update(cx, |input, cx| {
input.set_placeholder(self.i18n.t("search_placeholder"), window, cx);
});

// Sync auto-start state with system
if let Err(e) = self.sync_autostart_state() {
eprintln!("[ropy] Failed to sync auto-start state: {e}");
Expand Down Expand Up @@ -291,7 +318,7 @@ impl Render for RopyBoard {
.on_action(cx.listener(Self::on_select_next))
.on_action(cx.listener(Self::on_confirm_selection))
.on_key_down(cx.listener(Self::on_key_down))
.child(render_header(self.pinned, cx))
.child(render_header(self, cx))
.child(render_search_input(&self.search_input, cx))
.child(self.render_records_list(cx))
}
Expand Down
19 changes: 12 additions & 7 deletions src/gui/board/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ fn get_hex_color(content: &str) -> Option<gpui::Rgba> {
}

/// Create the "Clear" button element
pub(super) fn create_clear_button(cx: &mut Context<'_, RopyBoard>) -> impl IntoElement {
pub(super) fn create_clear_button(board: &RopyBoard, cx: &mut Context<'_, RopyBoard>) -> impl IntoElement {
Button::new("clear-button")
.ghost()
.icon(Icon::empty().path("clear-all.svg"))
.tooltip("Clear All")
.tooltip(board.i18n.t("clear_all"))
.on_click(cx.listener(|this, _, _, _| {
this.clear_history();
this.clear_last_copy_state();
Expand All @@ -65,8 +65,13 @@ pub(super) fn format_clipboard_content(record: &ClipboardRecord) -> String {
}

/// Render the header section with title and settings/clear buttons
pub fn render_header(pinned: bool, cx: &mut Context<'_, RopyBoard>) -> impl IntoElement {
let is_pinned = pinned;
pub fn render_header(board: &RopyBoard, cx: &mut Context<'_, RopyBoard>) -> impl IntoElement {
let is_pinned = board.pinned;
let pin_tooltip = if is_pinned {
board.i18n.t("unpin")
} else {
board.i18n.t("pin")
};
let header = h_flex().justify_between().items_center().mb_4().pt_4();

#[cfg(target_os = "windows")]
Expand All @@ -80,7 +85,7 @@ pub fn render_header(pinned: bool, cx: &mut Context<'_, RopyBoard>) -> impl Into
.text_lg()
.text_color(cx.theme().foreground)
.font_weight(gpui::FontWeight::BOLD)
.child("Ropy"),
.child(board.i18n.t("app_name")),
)
.child(
h_flex()
Expand All @@ -93,7 +98,7 @@ pub fn render_header(pinned: bool, cx: &mut Context<'_, RopyBoard>) -> impl Into
Button::new("pin-button").ghost()
}
.icon(Icon::empty().path("pin-to-top.svg"))
.tooltip("Pin to top")
.tooltip(pin_tooltip)
.on_click(cx.listener(|this, _, window, cx| {
this.pinned = !this.pinned;
set_always_on_top(window, cx, this.pinned);
Expand All @@ -116,7 +121,7 @@ pub fn render_header(pinned: bool, cx: &mut Context<'_, RopyBoard>) -> impl Into
}))
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation()),
)
.child(create_clear_button(cx)),
.child(create_clear_button(board, cx)),
)
}

Expand Down
Loading