diff --git a/agent-ui/src/commonMain/composeResources/values-zh/strings.xml b/agent-ui/src/commonMain/composeResources/values-zh/strings.xml
index 003616278a..0c2b79df39 100644
--- a/agent-ui/src/commonMain/composeResources/values-zh/strings.xml
+++ b/agent-ui/src/commonMain/composeResources/values-zh/strings.xml
@@ -37,4 +37,8 @@
使用 Google 登录
正在等待浏览器 — 授权后请返回 CodeAssist
取消
+
+ CA 证书(可选)
+
+ 仅当端点使用系统不信任的私有或区域性 CA 时(如 GigaChat)才粘贴 CA 证书(PEM)。它会在系统 CA 之外额外受信任;证书链仍会校验。
diff --git a/ide-ui/src/commonMain/composeResources/values-zh/strings.xml b/ide-ui/src/commonMain/composeResources/values-zh/strings.xml
index 0fc41b6a9e..9b24b689d2 100644
--- a/ide-ui/src/commonMain/composeResources/values-zh/strings.xml
+++ b/ide-ui/src/commonMain/composeResources/values-zh/strings.xml
@@ -1267,4 +1267,863 @@
变量
调用
注释
+
+ 关闭项目
+
+ 返回全部项目
+
+ 管理依赖项
+
+ 设置与工具
+
+ 设置 · 代码样式 · SDK 管理器 · 密钥库管理器
+
+ 图标管理器
+
+ 浏览并导入图标,更换应用图标
+
+ 查看日志
+
+ 编辑器、分析与构建日志——异常时分享排查
+
+ 模块
+
+ 添加/移除模块 · Java 版本 · 依赖项 · 仓库
+
+ 重建项目索引
+
+ 重建符号与补全索引
+
+ 切换主题
+
+ 在浅色与深色之间切换
+
+ 应用图标已更新
+
+ 应用图标
+
+ 背景
+
+ 选择一个图标
+
+ 选择一张图片
+
+ 当前图标
+
+ %1$d 个文件
+
+ 前景
+
+ Play 商店图(512px)
+
+ 多密度 PNG(Android 8 之前)
+
+ 圆形图标
+
+ 清单:%1$s
+
+ 圆形
+
+ 圆角
+
+ 方形
+
+ 方圆形
+
+ 模块
+
+ 主题层
+
+ 此项目没有 Android 应用模块。
+
+ 无
+
+ 水平移动
+
+ 垂直移动
+
+ 将写入的内容
+
+ 预览
+
+ 将替换 %1$d 个现有文件
+
+ 与前景相同
+
+ 缩放
+
+ 主题化
+
+ 应用图标
+
+ 使用项目 drawable
+
+ 退出全屏
+
+ 全屏
+
+ 将 GitHub 或任意 Git 服务器上的仓库复制为新项目。
+
+ 从 Git 克隆
+
+ 返回搜索
+
+ BOM
+
+ BoM + Analytics
+
+ 融合定位
+
+ Google Maps SDK
+
+ %1$s · %2$s
+
+ 其他来源
+
+ Google 登录
+
+ 快速添加
+
+ 搜索或粘贴 group:name:version
+
+ 作用域与变体选项
+
+ 本地 .jar 或 .aar 文件
+
+ 本项目中的另一个模块
+
+ +%1$s
+
+ 详细信息
+
+ 构建输出、缓存和 SDK 绝不会打包。
+
+ Gradle 项目
+
+ 一个 .zip,包含你的源码和生成的 Gradle 构建文件,供 Android Studio 或 gradle 使用。
+
+ CodeAssist 安装包
+
+ 任何装有 CodeAssist 的人都能原样导入的 .caproj 文件。
+
+ 格式
+
+ 尽力而为。构建文件由项目模型生成,而非来自实际运行过的构建,因此请先通读再依赖它们。依赖项以 Maven 坐标声明,Gradle 在首次同步时下载,因此同步需要联网。
+
+ settings.gradle.kts、每个模块的 build.gradle.kts、gradle.properties 以及 Gradle wrapper 设置,全部由你的项目生成:模块、依赖项、SDK 版本、构建类型、变体和构建特性。
+
+ 会生成什么
+
+ 不包含 gradlew 脚本:Android Studio 会在首次同步时添加,或手动运行一次 gradle wrapper。
+
+ 将此项目写为 Gradle 构建,以便在 Android Studio 中打开或用 gradle 构建。
+
+ 排除一个模块时,依赖它的所有内容也会一并排除。
+
+ 模块
+
+ 这些内容也包含在压缩包内的 GRADLE-EXPORT.md 中。
+
+ 值得检查
+
+ 选项
+
+ 添加图片
+
+ 移除截图
+
+ 展示给导入此安装包的人。
+
+ 截图
+
+ 约 %1$s
+
+ 你的 Gradle 项目已就绪。
+
+ AIDL 文件
+
+ 图片资源
+
+ FTP 服务器
+
+ 运行于 127.0.0.1:8021 · 上传到 assets/,此设备上的任何应用均可写入
+
+ 已停止 · 本地资源上传服务器
+
+ 转换
+
+ 转换
+
+ Gradle 构建文件会移至备份文件夹,由 CodeAssist 自己的模块系统接管。之后将无法再从 Gradle 重新同步。此操作可以撤销。
+
+ 完成
+
+ 无法转换此项目。
+
+ 导入器尽力读取了以下内容——转换后不会再检查:
+
+ 转换为 CodeAssist 项目?
+
+ 撤销
+
+ 正在转换…
+
+ 已转换为 CodeAssist 项目。
+
+ 构建文件自上次同步后已更改。请重新同步以更新项目。
+
+ 发布会将你的项目放入商店,供任何人安装。以下是从头到尾的流程。
+
+ 每个压缩包 5 MB,最多 2000 个文件。每个账号可发布十个项目,同时最多三份提交等待审核。
+
+ 限制说明
+
+ 项目始终属于你。提交等待审核期间可以撤回,已发布的也可以申请下架。
+
+ 你的权益
+
+ 通过审核的项目会出现在"探索"中,任何人都可以安装。你可以继续发布新版本;旧版本仍然可用。
+
+ 通过后带着你的名字上架
+
+ 批准与否,决定都会以通知的形式送达,即使应用已关闭。
+
+ 无论结果如何都会通知你
+
+ 构建输出、缓存和 .git 不会包含在内。密钥也一样:keystore、local.properties、.env 和 google-services.json 都会被排除。上传前你可以看到确切的文件清单和被剔除的内容。
+
+ 项目在本设备上打包为压缩文件
+
+ 未经批准,任何内容都不会公开。审核员会下载压缩包、阅读你写的内容,然后选择发布或附上说明退回。
+
+ 会有真人进行审核
+
+ 发布流程是怎样的
+
+ 账号与工具
+
+ 备份项目
+
+ 取消
+
+ 从 Git 克隆
+
+ 删除
+
+ 删除项目
+
+ 这会从此设备上移除该项目及其全部内容。此操作无法撤销。
+
+ 删除 %1$s?
+
+ 关闭
+
+ 此列表暂无内容
+
+ 发送反馈
+
+ 从商店模板开始
+
+ 下午好
+
+ 晚上好
+
+ 早上好
+
+ 导入项目
+
+ 旧版本的项目可以恢复。
+
+ 更多操作
+
+ 新建项目
+
+ 骨架在本地生成,无需联网。
+
+ 打开
+
+ 在文件管理中打开存储
+
+ 打开 %1$s
+
+ 从上次离开的地方继续
+
+ 项目
+
+ 收藏
+
+ 更新
+
+ 设置与工具
+
+ 分享
+
+ 赞助 CodeAssist
+
+ 去 GitHub 加星
+
+ 你的项目
+
+ 应用图标
+
+ 预览并替换启动器图标
+
+ 应用图标
+
+ 颜色
+
+ 保持原色
+
+ 添加 androidx.compose.material:material-icons-extended 即可浏览全部图标。
+
+ 此模块的类路径上没有 Compose 图标库。
+
+ %1$d 种配置
+
+ %1$s 已声明此资源。替换后所有引用都会更新。
+
+ 替换 %1$s?
+
+ 已复制到剪贴板
+
+ 复制
+
+ 没有符合搜索条件的图标。
+
+ 填充
+
+ 添加到项目
+
+ 导入 SVG
+
+ 插入到光标处
+
+ 许可协议:%1$s
+
+ 加载 %1$s 图标
+
+ 正在下载图标列表…
+
+ 资源名称
+
+ 尚未注册任何图标库。
+
+ 此项目还没有 drawable 或 mipmap 资源。
+
+ 引用
+
+ 替换
+
+ 文件夹
+
+ 搜索图标
+
+ 大小
+
+ 样式
+
+ 描边
+
+ 圆角
+
+ 直角
+
+ Compose
+
+ 图标库
+
+ 项目
+
+ 写入位置
+
+ 图标管理器
+
+ 项目名称
+
+ 导入为
+
+ 将存放到 %1$s
+
+ 兼容模式
+
+ 保留 Gradle 文件并从中重新同步。之后可以再转换。
+
+ 转换为 CodeAssist
+
+ 切换到 CodeAssist 的模块系统。Gradle 文件会移至备份文件夹。
+
+ CodeAssist 应如何打开此项目?
+
+ 导入 Gradle 项目
+
+ 隐藏文件
+
+ 显示全部 %1$d 个文件
+
+ 项目文件夹
+
+ CodeAssist 项目,或现有的 Gradle 构建
+
+ 项目在哪里?
+
+ 项目安装包
+
+ 从 CodeAssist 分享或导出的 .caproj 文件
+
+ 导入项目
+
+ 已完成 %1$d 节课
+
+ %2$d 节课中的第 %1$d 节 · %3$d 分钟
+
+ 继续第 %1$d 节课
+
+ 点赞
+
+ 已点赞
+
+ 登录后点赞
+
+ 放弃
+
+ 有未保存的更改
+
+ Parcelable
+
+ 新建 AIDL 文件
+
+ IMyService
+
+ 清除
+
+ %1$d 天前
+
+ 关闭
+
+ 暂无新动态。
+
+ 审核结果、更新和完成的构建会显示在这里。
+
+ %1$d 小时前
+
+ 刚刚
+
+ 全部标为已读
+
+ %1$d 分钟前
+
+ 打开
+
+ 通知
+
+ 你将不会收到通知。
+
+ 刚才未能保存此设置。
+
+ 项目上架时会通知你。
+
+ 加载失败:%1$s
+
+ 还没有安装任何插件。从应用商店安装的 CodeAssist 插件会显示在这里。
+
+ 内置
+
+ 已安装
+
+ 关注
+
+ %1$d 位关注者
+
+ 已关注
+
+ 安装量
+
+ 获赞
+
+ 没有这个名称的发布者
+
+ 还没有发布任何内容
+
+ 项目
+
+ 已发布的项目
+
+ 评分
+
+ 登录后关注
+
+ 未评分
+
+ 发布到商店
+
+ 分享给所有人,而不只是一个人。发布前会先经审核。
+
+ 你的回复会显示在评价下方,并标注为发布者。
+
+ 发布回复
+
+ 以发布者身份回复
+
+ 告诉我们它有什么问题。审核员会阅读每一份举报。
+
+ 还有什么需要审核员知道的吗?(可选)
+
+ 错误或误导
+
+ 版权问题
+
+ 辱骂或不当内容
+
+ 恶意软件
+
+ 其他
+
+ 垃圾信息或广告
+
+ 发送举报
+
+ 正在发送…
+
+ 举报此评价
+
+ 评价管理
+
+ 已隐藏。该评价不再计入评分。
+
+ 隐藏评价
+
+ 回复
+
+ 删除回复
+
+ 编辑回复
+
+ 举报
+
+ 已举报。审核员会查看。
+
+ 恢复评价
+
+ %1$d 条评价
+
+ 1 条评价
+
+ 删除
+
+ 编辑你的评价
+
+ 有帮助
+
+ 有帮助(%1$d)
+
+ 暂无评价
+
+ 来当第一个说说这个项目用起来如何的人。
+
+ 于 v%1$s
+
+ 有什么想告诉下一位用户的吗?(可选)
+
+ 正在发布…
+
+ 一位 CodeAssist 用户
+
+ 发布者已回复
+
+ 登录后投票
+
+ 登录后写评价
+
+ 最有帮助
+
+ 最新
+
+ %1$d 星(满分 5 星)
+
+ 发布评价
+
+ 评分与评价
+
+ 评价功能暂时不可用。
+
+ 写评价
+
+ 你的评分
+
+ 你的评价
+
+ 横向滚动条
+
+ 当一行超出视图时,在底边显示可拖动的滚动条(自动换行开启时无需滚动)
+
+ Android 应用
+
+ Android 库
+
+ Java 库
+
+ 查看原因
+
+ 使用
+
+ 查看列表页
+
+ 查看备注
+
+ 撤回
+
+ 已发布 %1$d 个项目
+
+ 书架第一
+
+ 新
+
+ 按类型浏览
+
+ 随安装附带、可离线使用的项目骨架。
+
+ 内置于你的 IDE
+
+ 最新优先
+
+ 精选集
+
+ %1$d%%
+
+ 模板、示例应用和插件只要通过审核就会出现在这里。你可以成为第一个。
+
+ 还没有人发布项目
+
+ 项目积累一周的安装量后,排行榜就会出现。
+
+ 精选书架由团队从已发布的项目中挑选组成。
+
+ 需要 %2$d 个项目,已有 %1$d 个
+
+ 接下来会上架什么
+
+ 推荐需要你的安装历史——先安装一个项目吧。
+
+ 每个书架在内容足够参与排名时自动开启。
+
+ 商店成长后将陆续解锁
+
+ 排行榜需要至少 %1$d 个项目才启动——四个项目的榜单就是上面的列表。
+
+ 项目足够多、能凑出一个主题时,团队就会组建精选集。
+
+ 推荐需要跨越多个项目的安装历史。
+
+ 根据你的安装推荐…
+
+ 下载量
+
+ 商店已开放
+
+ 安装量
+
+ 如何运作
+
+ 发布流程是怎样的
+
+ 安装
+
+ 安装项目
+
+ %1$d 条评价
+
+ 大小
+
+ 更新日志
+
+ 概览
+
+ README
+
+ 评分与评价
+
+ 版本
+
+ 离线
+
+ 没有与"%1$s"匹配的项目
+
+ 暂无评分
+
+ 通知已关闭
+
+ 通知已开启
+
+ 只发一条通知,仅限首批上架。
+
+ 有项目上架时通知我
+
+ 打开
+
+ 在编辑器中打开
+
+ 五
+
+ 四
+
+ 二
+
+ 三
+
+ 与审核团队直接沟通
+
+ 发布首日即上门户首页
+
+ 现在已发布的所有项目都在这个页面上——没有要爬的排名,也没有藏在后面的分页。以后就不一定了。
+
+ 书架还没满
+
+ %1$s 下一个就是你的。
+
+ %1$s 你的将是第 %2$s 个。
+
+ 已收录 %1$d 个项目。
+
+ 已收录 1 个项目。
+
+ 再发布一个
+
+ 发布项目
+
+ 发布指南
+
+ 重试
+
+ 模板、示例应用、插件
+
+ 查看全部
+
+ 正在登录…
+
+ 暂不
+
+ 登录未完成
+
+ 使用 GitHub 继续
+
+ 使用 Google 继续
+
+ 重新打开浏览器
+
+ 退出登录
+
+ 已登录为 %1$s
+
+ 接下来是项目提交页。你的账号已就绪。
+
+ 发布到商店
+
+ 此构建版本未配置商店账号服务,因此发布功能已关闭。商店的其余功能均可正常使用。
+
+ 登录不可用
+
+ 在浏览器中完成登录后返回。此页面会自动更新。
+
+ 等待 GitHub
+
+ 发布需要账号,这样项目才有归属者,审核员也有对象可沟通。浏览和安装则从不需要。
+
+ 正在干净的机器上构建
+
+ 需要修改——请查看备注
+
+ 已在商店上架
+
+ 未通过
+
+ 未通过 · %1$s
+
+ 审核中 · 通常 2 个工作日
+
+ 标题、一句话简介、分类和截图——直接取自应用内的编辑器。
+
+ 填写列表信息
+
+ 选择任意本地项目,CodeAssist 会替你读取名称、语言、JDK 和 Gradle 配置——无需手写任何内容。
+
+ 从“项目”页选择一个项目
+
+ 我们会在干净的机器上构建,并由真人检查许可协议与 README。通常两个工作日。
+
+ 提交审核
+
+ 排行榜
+
+ 正在解压…
+
+ 使用
+
+ 添加截图
+
+ 列表信息
+
+ 无需剔除任何内容。
+
+ 未包含在上传中
+
+ 密钥和构建输出绝不会被上传。以下文件已被识别并剔除:
+
+ 分类
+
+ 描述
+
+ 一句话简介
+
+ 标签,逗号分隔
+
+ 标题
+
+ 版本
+
+ %1$d 个文件 · %2$s
+
+ 你的提交
+
+ 你还没有可发布的项目。
+
+ 正在打包…
+
+ 选择哪个项目?
+
+ 请填写标题、简介、描述和分类。
+
+ 每份提交都会经过审核员审核后才会展示。未经批准,任何内容都不会公开。
+
+ 截图
+
+ 最多六张。这是别人在安装之前最先看的东西。
+
+ 提交审核
+
+ 正在提交…
+
+ 登录后才能发布。
+
+ 发布项目
+
+ 此构建版本不支持发布。
+
+ 撤回
+
+ 在版本匹配之前,此模块的构建会一直失败。
+
+ 仍然构建
+
+ %1$d 个模块的运行时与内置的处理器版本不一致
+
+ 处理中…
+
+ 克隆自 %1$s
+
+ 仅以编辑方式打开
+
+
+ - %1$d 个项目
+ - %1$d 个项目
+
diff --git a/ide-ui/src/commonMain/composeResources/values/strings.xml b/ide-ui/src/commonMain/composeResources/values/strings.xml
index d2594b1828..028c8977e3 100644
--- a/ide-ui/src/commonMain/composeResources/values/strings.xml
+++ b/ide-ui/src/commonMain/composeResources/values/strings.xml
@@ -1604,4 +1604,163 @@ We never collect your code, file or project names, which features you use, or an
Reference
App icon
No icon libraries are registered.
+
+ Close project
+
+ Back to all projects
+
+ Manage dependencies
+
+ Settings & Tools
+
+ Settings · code style · SDK manager · keystore manager
+
+ Icon Manager
+
+ Browse and import icons, and change the app icon
+
+ View logs
+
+ Editor, analysis & build logs — share when something's off
+
+ Modules
+
+ Add/remove modules · Java version · dependencies · repositories
+
+ Re-index project
+
+ Rebuild symbol & completion indexes
+
+ Toggle theme
+
+ Switch between light and dark
+
+ Read why
+
+ Use
+
+ View listing
+
+ View notes
+
+ Withdraw
+
+ %1$d projects published
+
+ FIRST ON THE SHELF
+
+ NEW
+
+ Offline scaffolds that ship with the install.
+
+ Bundled with your IDE
+
+ newest first
+
+ Collections
+
+ Templates, sample apps and plugins show up here the moment they pass review. Yours can be the first one.
+
+ No one has published a project yet
+
+ Ranks appear once projects have a week of install history.
+
+ Curated shelves are assembled by the team from published projects.
+
+ %1$d of %2$d projects needed
+
+ What lands here next
+
+ Recommendations need your install history — install something first.
+
+ Each shelf switches on when it has enough to rank.
+
+ What unlocks as the store grows
+
+ Ranking starts at %1$d projects — a chart of four is just the list above.
+
+ The team builds a collection once there are enough projects to theme one.
+
+ Recommendations need install history across several projects.
+
+ Because you installed…
+
+ The store is open
+
+ How it works
+
+ How publishing works
+
+ offline
+
+ Not rated yet
+
+ Notifications off
+
+ Notifications on
+
+ One notification, only for the first batch.
+
+ Tell me when projects arrive
+
+ fifth
+
+ fourth
+
+ second
+
+ third
+
+ Direct line to the review team
+
+ Front page from day one
+
+ Everything published right now sits on this page — no ranking to climb, no back pages to fall into. That stops being true later.
+
+ Shelves are still short
+
+ %1$s Yours would be next.
+
+ %1$s Yours would be the %2$s.
+
+ %1$d projects in.
+
+ One project in.
+
+ Publish another
+
+ Publish a project
+
+ Publishing guide
+
+ Building on a clean machine
+
+ Changes requested — read the notes
+
+ Live in the store
+
+ Not accepted
+
+ Not accepted · %1$s
+
+ In review · usually 2 business days
+
+ Title, a short description, category and screenshots — captured from your editor inside the app.
+
+ Fill in the listing
+
+ Choose any local project. CodeAssist reads its name, language, JDK and Gradle setup for you — nothing to write by hand.
+
+ Pick a project from Home
+
+ We build it on a clean machine and a human checks licensing and the README. Usually two business days.
+
+ Submit for review
+
+ Top charts
+
+
+ - %1$d PROJECT
+ - %1$d PROJECTS
+
diff --git a/ide-ui/src/commonMain/kotlin/dev/ide/ui/components/StoreEmpty.kt b/ide-ui/src/commonMain/kotlin/dev/ide/ui/components/StoreEmpty.kt
index 5698214292..87c943da61 100644
--- a/ide-ui/src/commonMain/kotlin/dev/ide/ui/components/StoreEmpty.kt
+++ b/ide-ui/src/commonMain/kotlin/dev/ide/ui/components/StoreEmpty.kt
@@ -39,6 +39,37 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.ide.ui.backend.UiStoreSubmission
import dev.ide.ui.backend.UiSubmissionStatus
+import dev.ide.ui.generated.resources.Res
+import dev.ide.ui.generated.resources.store_empty_hero_body
+import dev.ide.ui.generated.resources.store_empty_hero_title
+import dev.ide.ui.generated.resources.store_ghost_charts_note
+import dev.ide.ui.generated.resources.store_ghost_collections_note
+import dev.ide.ui.generated.resources.store_ghost_recommend_note
+import dev.ide.ui.generated.resources.store_hero_eyebrow_open
+import dev.ide.ui.generated.resources.store_notify_state_off
+import dev.ide.ui.generated.resources.store_notify_state_on
+import dev.ide.ui.generated.resources.store_notify_subtitle
+import dev.ide.ui.generated.resources.store_notify_title
+import dev.ide.ui.generated.resources.store_publish_another
+import dev.ide.ui.generated.resources.store_publish_cta
+import dev.ide.ui.generated.resources.store_publishing_guide
+import dev.ide.ui.generated.resources.store_action_read_why
+import dev.ide.ui.generated.resources.store_action_view_listing
+import dev.ide.ui.generated.resources.store_action_view_notes
+import dev.ide.ui.generated.resources.store_action_withdraw
+import dev.ide.ui.generated.resources.store_status_building
+import dev.ide.ui.generated.resources.store_status_changes
+import dev.ide.ui.generated.resources.store_status_published
+import dev.ide.ui.generated.resources.store_status_rejected
+import dev.ide.ui.generated.resources.store_status_rejected_note
+import dev.ide.ui.generated.resources.store_status_submitted
+import dev.ide.ui.generated.resources.store_step_listing_body
+import dev.ide.ui.generated.resources.store_step_listing_title
+import dev.ide.ui.generated.resources.store_step_pick_body
+import dev.ide.ui.generated.resources.store_step_pick_title
+import dev.ide.ui.generated.resources.store_step_submit_body
+import dev.ide.ui.generated.resources.store_step_submit_title
+import org.jetbrains.compose.resources.stringResource
import dev.ide.ui.icons.CaSymbols
import dev.ide.ui.theme.Symbol
import dev.ide.ui.theme.tileShape
@@ -85,9 +116,9 @@ fun EmptyStoreHero(
modifier = Modifier.align(Alignment.BottomEnd).offset(x = 24.dp, y = 34.dp),
)
Column(Modifier.padding(horizontal = 20.dp, vertical = 24.dp)) {
- Eyebrow("The store is open", color = c.onPrimaryContainer.copy(alpha = 0.75f))
+ Eyebrow(stringResource(Res.string.store_hero_eyebrow_open), color = c.onPrimaryContainer.copy(alpha = 0.75f))
Text(
- "No one has published a project yet",
+ stringResource(Res.string.store_empty_hero_title),
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.Medium, fontSize = 26.sp, lineHeight = 32.sp,
letterSpacing = (-0.7).sp,
@@ -96,8 +127,7 @@ fun EmptyStoreHero(
modifier = Modifier.padding(top = 8.dp),
)
Text(
- "Templates, sample apps and plugins show up here the moment they pass review. " +
- "Yours can be the first one.",
+ stringResource(Res.string.store_empty_hero_body),
style = MaterialTheme.typography.bodyMedium,
color = c.onPrimaryContainer.copy(alpha = 0.82f),
modifier = Modifier.padding(top = 10.dp),
@@ -121,7 +151,11 @@ fun EmptyStoreHero(
) {
Symbol(CaSymbols.upload, contentDescription = null, size = 20.dp)
Text(
- if (hasSubmission) "Publish another" else "Publish a project",
+ if (hasSubmission) {
+ stringResource(Res.string.store_publish_another)
+ } else {
+ stringResource(Res.string.store_publish_cta)
+ },
style = MaterialTheme.typography.labelLarge,
)
}
@@ -135,7 +169,7 @@ fun EmptyStoreHero(
modifier = Modifier.height(46.dp),
) {
Box(Modifier.padding(horizontal = 20.dp), contentAlignment = Alignment.Center) {
- Text("Publishing guide", style = MaterialTheme.typography.labelLarge)
+ Text(stringResource(Res.string.store_publishing_guide), style = MaterialTheme.typography.labelLarge)
}
}
}
@@ -189,19 +223,19 @@ fun PublishingSteps(steps: List, modifier: Modifier = Modifier) {
}
/** The default copy for [PublishingSteps]. Kept beside the component so the two cannot drift apart. */
-val defaultPublishSteps: List = listOf(
+@Composable
+fun defaultPublishSteps(): List = listOf(
PublishStep(
- "Pick a project from Home",
- "Choose any local project. CodeAssist reads its name, language, JDK and Gradle setup for you — " +
- "nothing to write by hand.",
+ stringResource(Res.string.store_step_pick_title),
+ stringResource(Res.string.store_step_pick_body),
),
PublishStep(
- "Fill in the listing",
- "Title, a short description, category and screenshots — captured from your editor inside the app.",
+ stringResource(Res.string.store_step_listing_title),
+ stringResource(Res.string.store_step_listing_body),
),
PublishStep(
- "Submit for review",
- "We build it on a clean machine and a human checks licensing and the README. Usually two business days.",
+ stringResource(Res.string.store_step_submit_title),
+ stringResource(Res.string.store_step_submit_body),
),
)
@@ -225,6 +259,9 @@ fun NotifySwitchRow(
message: String? = null,
) {
val c = MaterialTheme.colorScheme
+ // Resolved outside the semantics block: stringResource is composable-only.
+ val stateOn = stringResource(Res.string.store_notify_state_on)
+ val stateOff = stringResource(Res.string.store_notify_state_off)
Surface(
shape = RoundedCornerShape(26.dp),
color = c.surfaceContainerLow,
@@ -238,12 +275,12 @@ fun NotifySwitchRow(
) {
Column(Modifier.weight(1f)) {
Text(
- "Tell me when projects arrive",
+ stringResource(Res.string.store_notify_title),
style = MaterialTheme.typography.titleSmall,
color = c.onSurface,
)
Text(
- message ?: "One notification, only for the first batch.",
+ message ?: stringResource(Res.string.store_notify_subtitle),
style = MaterialTheme.typography.bodySmall,
// The failure takes over the supporting line rather than adding a row: it replaces the
// promise it just failed to keep.
@@ -257,7 +294,7 @@ fun NotifySwitchRow(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = Modifier.semantics {
- stateDescription = if (checked) "Notifications on" else "Notifications off"
+ stateDescription = if (checked) stateOn else stateOff
},
)
}
@@ -282,19 +319,36 @@ fun SubmissionStatusCard(
val c = MaterialTheme.colorScheme
val (line, actionLabel, action) = when (submission.status) {
UiSubmissionStatus.SUBMITTED ->
- Triple("In review · usually 2 business days", "Withdraw", onWithdraw)
+ Triple(
+ stringResource(Res.string.store_status_submitted),
+ stringResource(Res.string.store_action_withdraw),
+ onWithdraw,
+ )
UiSubmissionStatus.BUILDING ->
- Triple("Building on a clean machine", "Withdraw", onWithdraw)
+ Triple(
+ stringResource(Res.string.store_status_building),
+ stringResource(Res.string.store_action_withdraw),
+ onWithdraw,
+ )
UiSubmissionStatus.CHANGES_REQUESTED ->
- Triple("Changes requested — read the notes", "View notes", onViewNotes)
+ Triple(
+ stringResource(Res.string.store_status_changes),
+ stringResource(Res.string.store_action_view_notes),
+ onViewNotes,
+ )
UiSubmissionStatus.REJECTED ->
Triple(
- submission.note?.let { "Not accepted · $it" } ?: "Not accepted",
- "Read why",
+ submission.note?.let { stringResource(Res.string.store_status_rejected_note, it) }
+ ?: stringResource(Res.string.store_status_rejected),
+ stringResource(Res.string.store_action_read_why),
onViewNotes,
)
UiSubmissionStatus.PUBLISHED ->
- Triple("Live in the store", "View listing", onViewListing)
+ Triple(
+ stringResource(Res.string.store_status_published),
+ stringResource(Res.string.store_action_view_listing),
+ onViewListing,
+ )
}
// Only the in-flight states spin; a finished one holding an animation would read as still working.
val spinning = submission.status == UiSubmissionStatus.SUBMITTED ||
@@ -376,8 +430,9 @@ private fun SpinningGlyph(glyph: Char, spinning: Boolean, tint: Color) {
* Different from the sparse state's on purpose: with nothing published, the honest thing to state is the
* **condition** that fills the shelf, not a threshold the reader is nowhere near.
*/
+@Composable
fun emptyGhostNote(key: String): String = when (key) {
- "charts" -> "Ranks appear once projects have a week of install history."
- "collections" -> "Curated shelves are assembled by the team from published projects."
- else -> "Recommendations need your install history — install something first."
+ "charts" -> stringResource(Res.string.store_ghost_charts_note)
+ "collections" -> stringResource(Res.string.store_ghost_collections_note)
+ else -> stringResource(Res.string.store_ghost_recommend_note)
}
diff --git a/ide-ui/src/commonMain/kotlin/dev/ide/ui/components/StoreSparse.kt b/ide-ui/src/commonMain/kotlin/dev/ide/ui/components/StoreSparse.kt
index 6e2765e2fe..80f265d21c 100644
--- a/ide-ui/src/commonMain/kotlin/dev/ide/ui/components/StoreSparse.kt
+++ b/ide-ui/src/commonMain/kotlin/dev/ide/ui/components/StoreSparse.kt
@@ -36,6 +36,30 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.ide.ui.backend.UiGhostShelf
import dev.ide.ui.backend.UiStoreItem
+import dev.ide.ui.generated.resources.Res
+import dev.ide.ui.generated.resources.store_action_use
+import dev.ide.ui.generated.resources.store_badge_cd
+import dev.ide.ui.generated.resources.store_badge_first
+import dev.ide.ui.generated.resources.store_badge_new
+import dev.ide.ui.generated.resources.store_ghost_count_cd
+import dev.ide.ui.generated.resources.store_how_it_works
+import dev.ide.ui.generated.resources.store_not_rated
+import dev.ide.ui.generated.resources.store_ordinal_fifth
+import dev.ide.ui.generated.resources.store_ordinal_fourth
+import dev.ide.ui.generated.resources.store_ordinal_second
+import dev.ide.ui.generated.resources.store_ordinal_third
+import dev.ide.ui.generated.resources.store_perk_direct_line
+import dev.ide.ui.generated.resources.store_perk_front_page
+import dev.ide.ui.generated.resources.store_pitch_body
+import dev.ide.ui.generated.resources.store_pitch_eyebrow
+import dev.ide.ui.generated.resources.store_pitch_next
+import dev.ide.ui.generated.resources.store_pitch_ordinal_full
+import dev.ide.ui.generated.resources.store_pitch_subject
+import dev.ide.ui.generated.resources.store_pitch_subject_one
+import dev.ide.ui.generated.resources.store_project_count
+import dev.ide.ui.generated.resources.store_publish_cta
+import org.jetbrains.compose.resources.pluralStringResource
+import org.jetbrains.compose.resources.stringResource
import dev.ide.ui.icons.CaSymbols
import dev.ide.ui.theme.Symbol
import dev.ide.ui.theme.cardShape
@@ -59,18 +83,18 @@ import dev.ide.ui.theme.tonalPair
@Composable
fun StoreCountBadge(count: Int, modifier: Modifier = Modifier) {
val c = MaterialTheme.colorScheme
+ // Resolved outside the semantics block: stringResource is composable-only.
+ val cd = stringResource(Res.string.store_badge_cd, count)
+ val label = pluralStringResource(Res.plurals.store_project_count, count, count)
Surface(
shape = RoundedCornerShape(9.dp),
color = c.surfaceContainerHigh,
contentColor = c.onSurfaceVariant,
modifier = modifier.height(28.dp)
- .semantics { contentDescription = "$count projects published" },
+ .semantics { contentDescription = cd },
) {
Box(Modifier.padding(horizontal = 12.dp), contentAlignment = Alignment.Center) {
- Text(
- if (count == 1) "1 PROJECT" else "$count PROJECTS",
- style = MaterialTheme.typography.labelSmall,
- )
+ Text(label, style = MaterialTheme.typography.labelSmall)
}
}
}
@@ -232,7 +256,7 @@ private fun RatingLine(item: UiStoreItem, onContainer: Color) {
)
} else {
Text(
- "Not rated yet",
+ stringResource(Res.string.store_not_rated),
style = MaterialTheme.typography.labelLarge,
color = onContainer.copy(alpha = 0.75f),
)
@@ -248,9 +272,10 @@ private fun RatingLine(item: UiStoreItem, onContainer: Color) {
*
* Index 0 is the shelf's first project; index 1 gets "NEW" only when it is genuinely recent.
*/
+@Composable
private fun sparseBadge(index: Int, isRecent: Boolean): String? = when {
- index == 0 -> "FIRST ON THE SHELF"
- index == 1 && isRecent -> "NEW"
+ index == 0 -> stringResource(Res.string.store_badge_first)
+ index == 1 && isRecent -> stringResource(Res.string.store_badge_new)
else -> null
}
@@ -284,7 +309,7 @@ fun PublishPitchBand(
modifier = Modifier.align(Alignment.BottomEnd).offset(x = 20.dp, y = 26.dp),
)
Column(Modifier.padding(20.dp)) {
- Eyebrow("Shelves are still short", color = c.onPrimaryContainer.copy(alpha = 0.75f))
+ Eyebrow(stringResource(Res.string.store_pitch_eyebrow), color = c.onPrimaryContainer.copy(alpha = 0.75f))
Text(
pitchHeadline(projectCount),
style = MaterialTheme.typography.headlineSmall.copy(
@@ -295,8 +320,7 @@ fun PublishPitchBand(
modifier = Modifier.padding(top = 8.dp),
)
Text(
- "Everything published right now sits on this page — no ranking to climb, no back " +
- "pages to fall into. That stops being true later.",
+ stringResource(Res.string.store_pitch_body),
style = MaterialTheme.typography.bodyMedium,
color = c.onPrimaryContainer.copy(alpha = 0.82f),
modifier = Modifier.padding(top = 8.dp),
@@ -319,7 +343,7 @@ fun PublishPitchBand(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Symbol(CaSymbols.upload, contentDescription = null, size = 19.dp)
- Text("Publish a project", style = MaterialTheme.typography.labelLarge)
+ Text(stringResource(Res.string.store_publish_cta), style = MaterialTheme.typography.labelLarge)
}
}
Surface(
@@ -331,7 +355,7 @@ fun PublishPitchBand(
modifier = Modifier.height(44.dp),
) {
Box(Modifier.padding(horizontal = 18.dp), contentAlignment = Alignment.Center) {
- Text("How it works", style = MaterialTheme.typography.labelLarge)
+ Text(stringResource(Res.string.store_how_it_works), style = MaterialTheme.typography.labelLarge)
}
}
}
@@ -341,8 +365,8 @@ fun PublishPitchBand(
Modifier.fillMaxWidth().padding(top = 18.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
- PerkTile(CaSymbols.visibility, "Front page from day one", Modifier.weight(1f))
- PerkTile(CaSymbols.forum, "Direct line to the review team", Modifier.weight(1f))
+ PerkTile(CaSymbols.visibility, stringResource(Res.string.store_perk_front_page), Modifier.weight(1f))
+ PerkTile(CaSymbols.forum, stringResource(Res.string.store_perk_direct_line), Modifier.weight(1f))
}
}
}
@@ -373,16 +397,19 @@ private fun PerkTile(glyph: Char, label: String, modifier: Modifier = Modifier)
* Above five the ordinal starts sounding like a small club rather than an opportunity, so it switches to
* "Yours would be next" — the handoff's own note on the copy.
*/
+@Composable
internal fun pitchHeadline(count: Int): String {
- val subject = if (count == 1) "One project in." else "$count projects in."
+ val subject = if (count == 1) stringResource(Res.string.store_pitch_subject_one)
+ else stringResource(Res.string.store_pitch_subject, count)
val ordinal = when (count) {
- 1 -> "second"
- 2 -> "third"
- 3 -> "fourth"
- 4 -> "fifth"
+ 1 -> stringResource(Res.string.store_ordinal_second)
+ 2 -> stringResource(Res.string.store_ordinal_third)
+ 3 -> stringResource(Res.string.store_ordinal_fourth)
+ 4 -> stringResource(Res.string.store_ordinal_fifth)
else -> null
}
- return if (ordinal != null) "$subject Yours would be the $ordinal." else "$subject Yours would be next."
+ return if (ordinal != null) stringResource(Res.string.store_pitch_ordinal_full, subject, ordinal)
+ else stringResource(Res.string.store_pitch_next, subject)
}
/**
@@ -423,12 +450,14 @@ fun GhostShelfCard(
modifier = Modifier.weight(1f),
)
if (shelf != null) {
+ // Resolved outside the semantics block: stringResource is composable-only.
+ val shelfCountCd = stringResource(Res.string.store_ghost_count_cd, shelf.have, shelf.need)
Surface(
shape = RoundedCornerShape(8.dp),
color = c.surfaceContainer,
contentColor = c.onSurfaceVariant,
modifier = Modifier.height(24.dp).semantics {
- contentDescription = "${shelf.have} of ${shelf.need} projects needed"
+ contentDescription = shelfCountCd
},
) {
Box(Modifier.padding(horizontal = 10.dp), contentAlignment = Alignment.Center) {
@@ -497,7 +526,7 @@ fun BundledTemplateRow(
}
Surface(shape = CircleShape, color = c.primaryContainer, contentColor = c.onPrimaryContainer) {
Box(Modifier.height(34.dp).padding(horizontal = 15.dp), contentAlignment = Alignment.Center) {
- Text("Use", style = MaterialTheme.typography.labelMedium)
+ Text(stringResource(Res.string.store_action_use), style = MaterialTheme.typography.labelMedium)
}
}
}
diff --git a/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/EditorSheets.kt b/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/EditorSheets.kt
index cd40f51d60..c11f03fefa 100644
--- a/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/EditorSheets.kt
+++ b/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/EditorSheets.kt
@@ -151,7 +151,7 @@ internal fun MoreSheetContent(
Text(stringResource(Res.string.more), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(start = 6.dp, top = 4.dp, bottom = 10.dp))
actions.forEach { a ->
- MoreRow(actionIcon(a.iconId), a.text, a.description ?: "") { a.perform(host) }
+ MoreRow(actionIcon(a.iconId), localizedUiActionText(a), localizedUiActionDescription(a) ?: "") { a.perform(host) }
}
// Performance-analytics opt-in lives here (a settings surface) rather than on the home screen, so it's
diff --git a/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/ExploreFeedScreen.kt b/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/ExploreFeedScreen.kt
index 3492c38d38..73c19f3752 100644
--- a/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/ExploreFeedScreen.kt
+++ b/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/ExploreFeedScreen.kt
@@ -37,6 +37,20 @@ import dev.ide.ui.backend.UiStoreFeed
import dev.ide.ui.backend.UiStoreItem
import dev.ide.ui.backend.UiStoreMode
import dev.ide.ui.backend.UiStorePublisher
+import dev.ide.ui.generated.resources.store_bundled_subtitle
+import dev.ide.ui.generated.resources.store_bundled_title
+import dev.ide.ui.generated.resources.store_catalogue_subtitle
+import dev.ide.ui.generated.resources.store_collections_title
+import dev.ide.ui.generated.resources.store_ghost_next_title
+import dev.ide.ui.generated.resources.store_ghost_section_subtitle
+import dev.ide.ui.generated.resources.store_ghost_section_title
+import dev.ide.ui.generated.resources.store_ghostspec_charts_note
+import dev.ide.ui.generated.resources.store_ghostspec_collections_note
+import dev.ide.ui.generated.resources.store_ghostspec_rec_note
+import dev.ide.ui.generated.resources.store_ghostspec_rec_title
+import dev.ide.ui.generated.resources.store_how_publishing_title
+import dev.ide.ui.generated.resources.store_meta_offline
+import dev.ide.ui.generated.resources.store_top_charts
import dev.ide.ui.generated.resources.store_signin_title
import dev.ide.ui.components.SquareToneButton
import dev.ide.ui.components.inFlight
@@ -64,6 +78,7 @@ import dev.ide.ui.components.StoreCountBadge
import dev.ide.ui.components.TrendingTicker
import dev.ide.ui.components.chartMeta
import dev.ide.ui.components.motifFor
+import org.jetbrains.compose.resources.stringResource
import dev.ide.ui.icons.CaSymbols
import dev.ide.ui.theme.tonalPair
import dev.ide.ui.generated.resources.Res
@@ -210,7 +225,7 @@ fun ExploreFeed(
is UiFeedSection.Catalogue -> {
item("head_${section.id}") {
- SectionHeader(section.title, subtitle = "newest first")
+ SectionHeader(section.title, subtitle = stringResource(Res.string.store_catalogue_subtitle))
}
itemsIndexed(section.items, key = { _, it -> "cat_${it.id}" }) { i, item ->
Spacer(Modifier.height(12.dp))
@@ -243,15 +258,15 @@ fun ExploreFeed(
if (bundled.isNotEmpty()) {
item("head_${section.id}") {
SectionHeader(
- "Bundled with your IDE",
- subtitle = "Offline scaffolds that ship with the install.",
+ stringResource(Res.string.store_bundled_title),
+ subtitle = stringResource(Res.string.store_bundled_subtitle),
)
}
itemsIndexed(bundled, key = { _, it -> "bundled_${it.id}" }) { i, item ->
Spacer(Modifier.height(10.dp))
BundledTemplateRow(
title = item.title,
- meta = listOfNotNull(item.language, "offline").joinToString(" · "),
+ meta = listOfNotNull(item.language, stringResource(Res.string.store_meta_offline)).joinToString(" · "),
iconId = item.iconId,
index = i,
onUse = { onUseBundled(item) },
@@ -263,8 +278,8 @@ fun ExploreFeed(
is UiFeedSection.GhostShelves -> {
item("head_${section.id}") {
SectionHeader(
- "What unlocks as the store grows",
- subtitle = "Each shelf switches on when it has enough to rank.",
+ stringResource(Res.string.store_ghost_section_title),
+ subtitle = stringResource(Res.string.store_ghost_section_subtitle),
)
}
itemsIndexed(section.shelves, key = { _, it -> "ghost_${it.key}" }) { _, shelf ->
@@ -372,7 +387,7 @@ private fun ChartsSection(
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
- "Top charts",
+ stringResource(Res.string.store_top_charts),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f),
@@ -481,24 +496,25 @@ private data class GhostSpec(
val slotCount: Int,
)
+@Composable
private fun ghostSpec(key: String, need: Int): GhostSpec = when (key) {
"charts" -> GhostSpec(
- title = "Top charts",
- note = "Ranking starts at $need projects — a chart of four is just the list above.",
+ title = stringResource(Res.string.store_top_charts),
+ note = stringResource(Res.string.store_ghostspec_charts_note, need),
glyph = CaSymbols.trendingUp,
slotHeight = 46.dp,
slotCount = 3,
)
"collections" -> GhostSpec(
- title = "Collections",
- note = "The team builds a collection once there are enough projects to theme one.",
+ title = stringResource(Res.string.store_collections_title),
+ note = stringResource(Res.string.store_ghostspec_collections_note),
glyph = CaSymbols.rocketLaunch,
slotHeight = 72.dp,
slotCount = 2,
)
else -> GhostSpec(
- title = "Because you installed…",
- note = "Recommendations need install history across several projects.",
+ title = stringResource(Res.string.store_ghostspec_rec_title),
+ note = stringResource(Res.string.store_ghostspec_rec_note),
glyph = CaSymbols.hub,
slotHeight = 60.dp,
slotCount = 3,
@@ -568,22 +584,22 @@ private fun ExploreEmpty(
modifier = Modifier.padding(top = 4.dp),
)
}
- item("steps_head") { SectionHeader("How publishing works") }
- item("steps") { PublishingSteps(defaultPublishSteps) }
+ item("steps_head") { SectionHeader(stringResource(Res.string.store_how_publishing_title)) }
+ item("steps") { PublishingSteps(defaultPublishSteps()) }
}
if (bundled.isNotEmpty()) {
item("bundled_head") {
SectionHeader(
- "Bundled with your IDE",
- subtitle = "Offline scaffolds that ship with the install.",
+ stringResource(Res.string.store_bundled_title),
+ subtitle = stringResource(Res.string.store_bundled_subtitle),
)
}
itemsIndexed(bundled, key = { _, it -> "bundled_${it.id}" }) { i, item ->
Spacer(Modifier.height(10.dp))
BundledTemplateRow(
title = item.title,
- meta = listOfNotNull(item.language, "offline").joinToString(" · "),
+ meta = listOfNotNull(item.language, stringResource(Res.string.store_meta_offline)).joinToString(" · "),
iconId = item.iconId,
index = i,
onUse = { onUseBundled(item) },
@@ -591,7 +607,7 @@ private fun ExploreEmpty(
}
}
- item("ghosts_head") { SectionHeader("What lands here next") }
+ item("ghosts_head") { SectionHeader(stringResource(Res.string.store_ghost_next_title)) }
// No progress counters here: at zero there is nothing to be partway toward, so each note
// states the CONDITION that fills the shelf instead.
itemsIndexed(EMPTY_GHOSTS, key = { _, k -> "ghost_$k" }) { _, key ->
diff --git a/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/UiActionLocalization.kt b/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/UiActionLocalization.kt
new file mode 100644
index 0000000000..a7aa21bba3
--- /dev/null
+++ b/ide-ui/src/commonMain/kotlin/dev/ide/ui/screens/UiActionLocalization.kt
@@ -0,0 +1,54 @@
+package dev.ide.ui.screens
+
+import androidx.compose.runtime.Composable
+import dev.ide.ui.ext.UiHostAction
+import dev.ide.ui.generated.resources.*
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.stringResource
+
+/**
+ * A localization overlay for the built-in UI actions (the "More" menu rows and the command palette's
+ * UI-navigation commands). Their titles/descriptions are declared in `ide-ui-api`
+ * ([dev.ide.ui.ext.BuiltInUiPlugin]) as plain English — the plugin API carries plain strings (third-party
+ * plugins have no Compose-resource access), so rather than translate there, the UI maps a built-in action id
+ * to a string resource here and resolves it at render time.
+ *
+ * Same OVERRIDE pattern as [SettingsLocalization]: where a resource exists it wins, otherwise the
+ * plugin-supplied string is used verbatim, keeping third-party plugin actions working (unknown ids fall
+ * through).
+ *
+ * Keys mirror the [dev.ide.ui.ext.SimpleUiAction] ids.
+ */
+
+// Action title, by action id.
+private val ACTION_TITLE: Map = mapOf(
+ "ui.hub" to Res.string.action_hub,
+ "ui.modules" to Res.string.action_modules,
+ "ui.icons" to Res.string.action_icons,
+ "ui.dependencies" to Res.string.action_dependencies,
+ "ui.reindex" to Res.string.action_reindex,
+ "ui.logs" to Res.string.action_logs,
+ "ui.toggleTheme" to Res.string.action_toggle_theme,
+ "ui.closeProject" to Res.string.action_close_project,
+)
+
+// Action description, by action id (only the built-ins that carry one).
+private val ACTION_DESC: Map = mapOf(
+ "ui.hub" to Res.string.action_hub_desc,
+ "ui.modules" to Res.string.action_modules_desc,
+ "ui.icons" to Res.string.action_icons_desc,
+ "ui.reindex" to Res.string.action_reindex_desc,
+ "ui.logs" to Res.string.action_logs_desc,
+ "ui.toggleTheme" to Res.string.action_toggle_theme_desc,
+ "ui.closeProject" to Res.string.action_close_project_desc,
+)
+
+/** The localized title for a built-in UI action; the plugin-supplied title for a third-party action. */
+@Composable
+fun localizedUiActionText(action: UiHostAction): String =
+ ACTION_TITLE[action.id]?.let { stringResource(it) } ?: action.text
+
+/** The localized description for a built-in UI action, falling back to the plugin-supplied one (may be null). */
+@Composable
+fun localizedUiActionDescription(action: UiHostAction): String? =
+ ACTION_DESC[action.id]?.let { stringResource(it) } ?: action.description
diff --git a/vcs-ui/src/commonMain/composeResources/values-zh/strings.xml b/vcs-ui/src/commonMain/composeResources/values-zh/strings.xml
new file mode 100644
index 0000000000..a3326e66f9
--- /dev/null
+++ b/vcs-ui/src/commonMain/composeResources/values-zh/strings.xml
@@ -0,0 +1,211 @@
+
+
+
+ Git
+ 刷新
+ 取消
+ 复制
+ 已复制
+ 在浏览器中打开
+
+
+ 未纳入版本控制
+ 使用 Git 跟踪此项目,即可保留更改历史、在分支上工作并推送到 GitHub。
+ 创建仓库
+ 克隆仓库
+ 打开一个项目即可使用版本控制。
+ 没有可提交的内容
+ 你的工作区与最近一次提交一致。
+
+
+ 分支
+ 游离 HEAD
+ 尚无提交
+ 已是最新
+ 抓取
+ 拉取
+ 推送
+ 历史
+ 此文件的历史
+
+
+ 冲突
+ 已暂存
+ 更改
+ 暂存
+ 取消暂存
+ 全部暂存
+ 全部取消暂存
+ 丢弃
+ 丢弃更改?
+ 对 %1$s 的修改将被丢弃。此操作无法撤销。
+ 标记为已解决
+ 中止合并
+ 合并进行中
+ 变基进行中
+ Cherry-pick 进行中
+ 回退进行中
+ 二分查找进行中
+
+
+ 已添加
+ 已修改
+ 已删除
+ 已重命名
+ 已复制
+ 新增
+ 冲突
+
+
+ 提交信息
+ 提交
+ 提交并推送
+ 修正上次提交
+ 先暂存文件才能提交。
+ 提交前请先设置姓名和邮箱。
+ 设置身份信息
+ 姓名
+ 邮箱
+ 保存
+
+
+ 分支
+ 搜索分支
+ 本地
+ 远程
+ 没有匹配的分支。
+ 新建分支
+ 分支名称
+ 创建
+ 当前
+ 合并到 %1$s
+ 删除
+ 删除 %1$s?
+ 该分支将从本地移除。仅存在于该分支上的提交可能会丢失。
+ 仍要删除
+
+
+ 尚无提交。
+ 加载更多
+ %1$d 个文件更改
+ +%1$d
+ -%1$d
+ 合并
+
+
+ 差异
+ 没有文本差异。
+ 二进制文件,没有可显示的文本差异。
+
+
+ 贮藏
+ 贮藏列表
+ 贮藏更改
+ 描述(可选)
+ 包含未跟踪的文件
+ 应用
+ 丢弃
+ 没有贮藏的内容。
+
+
+ 添加默认 .gitignore 条目
+
+
+ 账号
+ 使用 GitHub 登录
+ 登录后可克隆你的私有仓库、通过 HTTPS 免密码推送,以及创建拉取请求。
+ 退出登录
+ 用于 Git 操作
+ 使用此账号
+ 在 GitHub 上输入此代码
+ 打开 %1$s 并输入代码。你批准后此页面会立即更新。
+ 等待批准…
+ 使用访问令牌
+ 个人访问令牌
+ 在 GitHub 的 Settings、Developer settings、Personal access tokens 中创建一个带有 "repo" 权限范围的令牌。
+ 登录
+ 其他 Git 服务器
+ 为不支持账号登录的服务器保存用户名和密码,例如自托管的 GitLab 或 Gitea。
+ 主机
+ git.example.com
+ 用户名
+ 密码或令牌
+ 已保存的服务器
+ 移除
+
+
+ 克隆仓库
+ 仓库 URL
+ 文件夹名称
+ 克隆
+ 你的仓库
+ 搜索仓库
+ 登录后即可浏览你的仓库。
+ 未找到仓库。
+ 无法识别的项目
+ %1$s 已克隆并添加到你的项目中,但没有构建系统能识别它。你可以浏览和编辑其文件;构建和运行需要先设置好模块。
+ 以编辑方式打开
+ 私有
+ Fork
+
+
+ GitHub
+ 发布到 GitHub
+ 为此项目创建一个仓库,并将当前分支推送到该仓库。
+ 仓库名称
+ 描述(可选)
+ 私有仓库
+ 发布
+ 发布前请先进行一次提交。
+ 拉取请求
+ 没有开启中的拉取请求。
+ 新建拉取请求
+ 标题
+ 描述
+ 合并到
+ 创建拉取请求
+ 来自 %1$s
+ 草稿
+
+
+ 远程仓库
+ 添加远程仓库
+ 远程仓库名称
+ 远程仓库 URL
+ 此项目还没有远程仓库。
+
+
+ 双方都修改了这些文件。逐个修复后,再将其标记为已解决。
+ 已就绪待提交。这些文件会进入你的下一次提交。
+ 已修改但尚未纳入。暂存文件即可将其加入下一次提交。
+ 提交会将已暂存的文件保存为快照,仅保存在本设备上。
+ 推送会将你的提交发送到远程仓库,让其他人可以看到。
+
+ 更多操作
+ 仓库
+ 此文件
+ 查看更改
+ 文件历史
+ 同步
+ 重新读取文件状态
+ 浏览历史提交
+ 检查远程是否有新提交,不改动你的文件
+ 将远程的提交拉入你的分支
+ 将你的提交发送到远程仓库
+ 你的 GitHub 账号、发布和拉取请求
+ 将此文件加入下一次提交
+ 将此文件移出下一次提交
+ 将所有已更改的文件加入下一次提交
+ 将所有文件移出下一次提交
+ 丢弃你对这个文件的修改
+ 按当前内容接受此文件
+ %1$s。点按可切换或新建分支。
+ 提交后推送
+ 未设置远程仓库
+ 连接到 GitHub
+ 退出 %1$s
+ 切换到此分支
+ 删除分支
+ 合并到当前分支
+