diff --git a/app/shared/app-lang/src/androidMain/res/values-zh-rCN/strings.xml b/app/shared/app-lang/src/androidMain/res/values-zh-rCN/strings.xml
index 9914198054..00918ab9c7 100644
--- a/app/shared/app-lang/src/androidMain/res/values-zh-rCN/strings.xml
+++ b/app/shared/app-lang/src/androidMain/res/values-zh-rCN/strings.xml
@@ -111,6 +111,43 @@
步骤 1:搜索条目
步骤 2:搜索剧集
步骤 3:匹配视频
+ 名称*
+ 设置显示在列表中的名称
+ 图标链接
+ 搜索链接
+ 示例:https://www.nyacg.net/search.html?wd={keyword}
+ 替换规则:\n{keyword} 替换为条目 (番剧) 名称
+ Base URL (可选)
+ 可选。用于拼接条目详情 (剧集列表) 页面 URL,将会影响步骤 2。默认自动从搜索链接生成
+ 仅使用第一个词
+ 以空格分割,仅使用第一个词搜索。适用于搜索兼容性差的情况
+ 去除特殊字符
+ 去除特殊字符以及 "电影" 等字样,提升搜索成功率
+ 尝试条目名称数量
+ 每次播放使用多少个条目名称进行查询。\n为 1 则只使用主名称,为 2 额外使用日文原名,大于 2 将额外使用其他别名,别名的数量不固定。\n一般用 1 就够了,使用多个名称将会显著增加播放时的等待时间。
+ 搜索请求间隔时间 (毫秒)
+ 控制每发送一个请求后等待多久后再发送下一个请求
+ 过滤设置
+ 使用条目名称过滤
+ 要求资源标题包含条目名称。适用于数据源可能搜到无关内容的情况。此功能只对 4.4.0 以前版本有效,对其他版本无效
+ 使用剧集序号过滤
+ 要求资源标题包含剧集序号。适用于数据源可能搜到无关内容的情况。通常建议开启
+ 标记分辨率
+ 将此数据源的资源都标记为该分辨率。不影响查询,只在播放器中选择数据源时用做偏好和过滤选项。
+ 标记字幕语言
+ 将此数据源的资源都标记为该字幕语言。不影响查询,只在播放器中选择数据源时用做偏好和过滤选项。
+ 在播放器内选择资源时
+ 区分条目名称
+ 关闭后,所有步骤 1 搜索到的条目都将被视为同一个,它们的相同标题的剧集将会被去重。\n开启此项则不会这样去重。\n此选项不影响测试结果,影响播放器内选择数据源时的结果。
+ 区分线路名称
+ 关闭后,线路名称不同,但只要标题相同的剧集就会被去重。\n开启此项则不会这样去重。\n此选项不影响测试结果,影响播放器内选择数据源时的结果。
+ 播放视频时
+ 播放视频时执行的 HTTP 请求的 Referer,可留空
+ 播放视频时执行的 HTTP 请求的 User-Agent
+ 单标签
+ 多标签
+ 不区分线路
+ 线路分组
启用嵌套链接
当遇到匹配的链接时,终止父页面加载并跳转到匹配的链接,在嵌套页面中继续查找视频链接。支持任意次数嵌套
匹配嵌套链接
@@ -192,6 +229,11 @@
已关闭,将会跳转到外部浏览器完成下载
自动下载更新
下载完成后会提示,确认后才会安装
+ 未找到安装包
+ 打开文件失败,请手动安装 %1$s
+ 查看安装包
+ 取消更新
+ 自动安装失败,请手动安装
检查中…
检查失败
已是最新
@@ -419,6 +461,19 @@
搜索
最高热度
新番时间表
+ 查看详情
+ 第 %1$s 话
+ 第 %1$s (%2$s) 话
+ 一
+ 二
+ 三
+ 四
+ 五
+ 六
+ 日
+ 周%1$s
+ 上周%1$s
+ 下周%1$s
继续观看
推荐
按住 Shift + 鼠标滚轮 同样可以水平滚动
@@ -486,6 +541,10 @@
自定义
自动检测结果
正在检测
+ 正在检测连接,请稍后
+ 部分服务连接失败,请考虑启用代理
+ 部分服务连接失败,请更换代理模式或代理地址
+ 所有服务连接正常
未检测到系统代理
代理地址
示例: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080
@@ -493,6 +552,15 @@
可选
无
密码
+ 重新测试
+ 弹幕服务
+ 收藏数据服务
+ 评论服务
+ 连接成功
+ 连接失败
+ 未启用代理
+ 正在使用%1$s
+ 保存并测试
anitorrent
@@ -524,6 +592,239 @@
已退出登录
账号
+
+ 欢迎使用 Animeko
+ 一站式在线弹幕追番平台 (简称 Ani)
+ Ani 目前由爱好者组成的组织 OpenAni 和社区贡献者维护,完全免费,在 GitHub 上开源。
+ Ani 的目标是提供尽可能简单且舒适的追番体验。
+ 继续
+ 主题设置
+ 网络设置
+ 欢迎,%1$s
+ 欢迎
+ 完成
+ 色彩
+ 动态色彩
+ 使用系统强调色
+ Ani 支持边下边播 BT 资源,BT 下载速度取决于网络质量
+ 允许通知权限,在缓存时查看下载进度
+ 启用 BitTorrent 功能
+ 允许通知
+ 显示 BT 下载进度和速度等信息
+ 已授权
+ 授予权限
+ 步骤 %1$d / %2$d
+ 下一步
+ 上一步
+ 跳过
+ 选择主题
+ 设置代理
+ BitTorrent 功能
+
+
+ 登录
+ 注册
+ 登录 / 注册
+ 绑定邮箱
+ 更改邮箱
+ 你的邮箱地址
+ 我们将发送一封验证码邮件
+ 邮箱
+ 清空
+ 继续
+ 该邮箱已被使用
+ 验证码无效或已过期,请重新发送
+ 输入验证码
+ 请检查邮箱 %1$s
+ 正在登录现有账号
+ 正在注册新账号
+ 验证码
+ 6 位数字
+ 重新发送验证码
+ %1$d 秒后可重新发送
+ 其他登录方式
+
+
+ 授权 Bangumi 登录
+ 授权 Bangumi 账号,可以同步你的观看记录到 Bangumi 或便捷登录 Ani
+ 绑定 Bangumi 账号
+ 登录 / 注册
+ 正在等待结果
+ 已授权
+ 取消
+ 帮助
+ Bangumi 是什么
+ 浏览器提示网站被屏蔽或禁止访问
+ 注册时应该选择哪一项
+ 注册或登录时一直提示验证码错误
+ 无法收到邮箱验证码
+ 注册时一直激活失败
+ 其他问题
+ Bangumi 番组计划是一个中文互联网的 ACGN 内容分享与交流网站,致力于提供一个轻松便捷独特的交流与沟通环境。\nBangumi 提供了番剧索引、番剧收藏、追番进度等功能,Ani 可以将你的观看记录同步至 Bangumi。
+ 请在系统设置中更换默认浏览器,推荐使用 Google Chrome、Microsoft Edge 或 Mozilla Firefox。
+ 选择“管理 ACG 收藏与收视进度,分享交流”这一项。
+ 如果没有验证码输入框,可以尝试多点几次密码输入框;如果输错了验证码,需要刷新页面后再重新登录。
+ 请检查垃圾箱,并尽可能使用常见邮箱注册,例如 QQ 邮箱、网易邮箱或 Outlook。
+ 删除激活码的最后一个字,然后手动输入删除的字,或更换其他浏览器。
+ 无法解决你的问题?还可以通过以下渠道获取帮助。
+
+
+ 适应
+ 填充
+ 裁切
+ 自动
+ 音轨
+ 关闭
+ 字幕
+ 禁用弹幕
+ 启用弹幕
+ 静音
+ 音量
+ 下一集
+ 选集
+ 发送
+ 倍速
+ 即将跳过 OP 或 ED
+ 取消
+ 来发一条弹幕吧~
+ 小心,我要发射弹幕啦!
+ 每一条弹幕背后,都有一个不为人知的秘密
+ 召唤弹幕精灵!
+ 这一刻的感受,只有你最懂
+ 让弹幕变得不一样
+ 弹幕世界大门已开
+ 字里行间,藏着宇宙的秘密
+ 在光与影的交织中,你的话语是唯一的真实
+ 有趣的灵魂万里挑一
+ 说点什么
+ 长期征集有趣的弹幕广告词
+ 广告位招租
+ 🤔
+ 梦开始的地方
+ 心念成形
+ 发个弹幕炒热气氛!
+ 来个弹幕吧!
+ 发个友善的弹幕吧!
+ 是不是忍不住想发弹幕了呢?
+
+
+ 想看
+ 在看
+ 看过
+ 搁置
+ 抛弃
+ 取消追番
+ 追番
+ 已想看
+ 已在看
+ 已看过
+ 已搁置
+ 已抛弃
+ 未追番
+ 取消追番
+ 这将会清除你的观看进度和评价。此操作无法撤销。确定要取消追番吗?
+ 删除
+ 取消
+ 要同时设置所有剧集为看过吗?
+ 设置
+ 忽略
+ 正在同步
+ 追番
+ 移至"看过"
+ 未收藏
+ 游客模式下请搜索后观看,或登录后使用收藏功能
+ %1$s 收藏 / %2$s 在看
+ / %1$s 抛弃
+ %1$s 年 %2$s 月
+
+
+ 取消看过
+ 已看过
+ 已抛弃
+ 看过
+ 选集播放
+ 长按还可以标记为已看
+ 条目详情
+ 关闭
+ 缓存
+ 收藏状态
+ 缓存类型
+ 下载状态
+ 下载中
+ 已完成
+ 排序
+ 最新下载
+ 最早下载
+ 条目名 A-Z
+ 条目名 Z-A
+ 剧集升序
+ 剧集降序
+ 想看
+ 在看
+ 看过
+ 搁置
+ 抛弃
+ 未收藏
+ 总上传
+ 总下载
+ 封面
+ 继续下载
+ 暂停下载
+ 管理此项
+ 下载完成
+ 下载失败
+ 下载中
+ 单集缓存
+ 删除
+ 取消
+ 缓存
+ 选择储存位置
+ 管理全部缓存
+ 未知
+ 选择一个条目查看具体缓存
+ %1$d 个已选
+ 退出选择
+ 选择所有
+ 删除所选
+ 进入选择模式
+ 删除缓存
+ 删除后不可恢复,确认删除吗?
+ %1$d/%2$d 已完成
+ %1$d 个下载中
+ 第%1$d话 · %2$s
+ 更多操作
+ 播放
+ 缓存信息无效,无法播放
+ 此资源不支持边下边播,请等待下载完成
+ 更多信息
+ 详情
+ 已复制
+ 复制
+ 打开链接
+ 打开文件失败:%1$s
+ 浏览文件
+ 剧集范围
+ 数据源
+ 在线
+ 本地
+ 字幕组
+ 字幕语言
+ 发布时间
+ 分辨率
+ 文件大小
+ 原始链接
+ 文件类型
+ 原始下载链接
+ 本地缓存路径
+ 总片段数
+ 下载器内部状态
+ 外挂字幕 %1$d
+ 未登录
+ 编辑个人资料
+ 登录 / 注册
+ 设置
+ 退出登录
+ 确定要退出登录吗?
Bangumi 登录过期
Bangumi 登录过期,请重新登录
@@ -539,6 +840,389 @@
确定要退出登录吗?
退出登录
取消
+ 手动全量同步
+ 重新下载全部 Bangumi 数据
+ 将 Bangumi 的收藏数据下载到 Animeko 收藏服务。通常来说不需要进行这个操作,Animeko 能自动完成同步。仅在你有发现数据不一致的情况时才需要手动下载。此操作可能需要数分钟才能完成,在同步过程中其他功能不可用。请注意,每十分钟只能执行一次全量同步
+ 同步队列
+ 待执行的同步操作
+ 执行全部
+ 加载中...
+ 加载中加载中加载中加载中...
+ 更新收藏:%1$d (%2$s)
+ 删除收藏:%1$d
+ 标记剧集为未看过:%1$d
+ 标记剧集为看过:%1$d (%2$s)
+ 未知操作(请更新版本)
+ 昵称
+ 未设置
+ 最多 20 字,只能包含中文、日文、英文、数字和下划线
+ 邮箱
+ 绑定
+ 用户 ID
+ 第三方账号
+ 未绑定
+ 解绑
+ 确定要解绑 Bangumi 吗?解绑后将不再同步观看记录到 Bangumi。解绑后可以重新绑定。
+ 选择头像
+ 完成
+ 上传头像
+ 选择文件
+ 支持 JPEG/PNG/WebP,最大 1MB。多次上传需间隔一分钟。
+ 或拖动文件到此处。支持 JPEG/PNG/WebP,最大 1MB。多次上传需间隔一分钟。
+ 正在上传...
+ 图片大小超过 1MB
+ 图片格式不支持
+ 裁剪并上传
+ 裁剪头像
+ 拖动选框移动,拖动角点调整大小
+ 将番剧收藏为 "%1$s" 后将在这里显示
+ 登录后可收藏
+ 即将上线,敬请期待
+ 写评价
+ 详情
+ 评价
+ 讨论
+ 角色
+ 角色 %1$d
+ 制作人员
+ 制作人员 %1$d
+ 关联条目
+ 显示更少
+ 显示更多
+ 查看全部
+ 前传
+ 续集
+ 衍生
+ 番外篇
+ 网络错误
+ 操作过快,请重试
+ 服务暂不可用
+ 无搜索结果
+ 此功能需要登录
+ 未知错误:%1$s
+ 无详细信息
+ 请求错误:%1$s
+ 已复制,请反馈到 GitHub issues 或群里
+ 发生未知错误,请在设置中反馈(附加日志)
+ 操作失败,请重试
+ 操作失败
+ 内嵌
+ 内封
+ 外挂
+ 未知
+ 内封或未知
+ 粤语
+ 简中
+ 繁中
+ 日语
+ 英语
+ 分辨率
+ 字幕
+ 字幕组
+ 展开
+ 清除筛选
+ 当前选中
+ 无字幕
+ 单集资源
+ 不支持播放
+ 季度不匹配
+ 条目标题不匹配
+ 请求无效,请检查
+ 保存并刷新
+ 编辑查询请求
+ 主搜索名
+ 大多数数据源只使用此名称
+ 次要搜索名
+ 在线源会忽略这些名称
+ 收起
+ 展开
+ 删除名称 %1$d
+ 添加名称
+ 剧集信息
+ 资源必须至少匹配以下两种信息中的一种,否则不会显示。可以只修改其中一种
+ 系列内剧集序号
+ 假设有两季,分别有 12 集,则第二季的第一集为 13
+ 条目内序号
+ 在当前季度内的序号,例如第二季的第一集为 01
+ 舍弃
+ 继续编辑
+ 有未保存的编辑,要舍弃编辑吗?
+ 都不对?
+ 修改查询
+ 正在等待验证码处理
+ 手动选择
+ 更换
+ 正在自动选择数据源
+ 请选择数据源
+ 数据源
+ 已查找:
+ 正在查询
+ 已查询
+ %1$s %2$d/%3$d 数据源
+ 帮助
+ 设置
+ 点击临时启用
+ 查询失败
+ 点击重试
+ 需要验证码
+ 点击验证
+ 查询成功
+ 验证
+ 数据源帮助
+ 数据源类型
+ 从 BitTorrent 网络获取资源,清晰度高,资源全面,加载速度可能不快
+ 从在线视频网站获取资源,加载速度快,但清晰度通常不高
+ 简单模式
+ 详细模式
+ 筛选到 %1$d/%2$d 条资源
+ 显示已被排除的资源 (%1$d)
+ 无法打开链接
+ 打开原始链接 %1$s
+ 编辑配置
+ 根据步骤 3 的配置,从 %1$d 个链接中未匹配到播放链接,请检查配置
+ 根据步骤 3 的配置,从 %1$d 个链接中匹配到了 %2$d 个播放链接
+ 根据步骤 3 的配置,从 %1$d 个链接中匹配到了 %2$d 个播放链接。为了更好的稳定性,建议调整规则,匹配到正好一个链接
+ 隐藏图片
+ 隐藏 CSS/字体
+ 隐藏 JS/WASM
+ 隐藏 data
+ 将实际播放:%1$s
+ 嵌套链接
+ 匹配
+ 未匹配
+ 第 %1$s 话
+ 已缓存
+ 弹幕数量
+ 已禁用
+ 设置 %1$s
+ 哔哩哔哩
+ 弹弹
+ 精确匹配
+ 半模糊匹配
+ 模糊匹配
+ 无匹配
+ 视频流链接
+ 种子文件下载链接
+ 本地文件链接
+ 磁力链接
+ 网页链接
+ 复制%1$s
+ 访问%1$s
+ 用其他应用打开
+ 复制数据源页面链接
+ 访问数据源页面
+ 剧集列表
+ 查看更多剧集
+ 继续观看 %1$s
+ 已看完
+ 还未开播
+ %1$s开播
+ 开始观看
+ %1$s更新
+ 看过 %1$s
+ 未知
+ 未开播
+ 连载中
+ 连载至 %1$s
+ 已完结
+ 全 %1$s 话
+ 预定全 %1$s 话
+ 评论
+ 评论 %1$d
+ 分享
+ 下载
+ 更换弹幕
+ 正在查询剧集列表…
+ 正在查询弹幕列表…
+ 选择条目
+ 选择剧集
+ 发送弹幕
+ 回到顶部
+ 已显示 %1$d 个结果
+ 最佳匹配
+ 最多收藏
+ 最高排名
+ 发布日期
+ 制作:
+ 受众
+ 分类
+ 角色
+ 情感
+ 类型
+ 分级
+ 地区
+ 系列
+ 设定
+ 来源
+ 技术
+ 自定义
+ 后台运行
+ 缓存功能需要应用保持在后台运行才能下载视频
+ 禁用电池优化
+ 可以帮助保持在后台运行。可能增加耗电
+ 通知设置
+ 打开设置
+ 弹幕刷新率
+ 分享当日日志文件
+ 分享日志文件
+ 复制当日日志内容(很大)
+ 未找到日志文件
+ 请先收藏再评分
+ 舍弃编辑
+ 评价尚未保存,确定要舍弃吗?
+ 舍弃
+ 修改评分
+ 评价
+ 说点什么...
+ 可留空
+ 仅自己可见
+ 不忍直视(请谨慎评价)
+ 很差
+ 差
+ 较差
+ 不过不失
+ 还行
+ 推荐
+ 力荐
+ 神作
+ 超神作(请谨慎评价)
+ 你的评分: %1$d
+ %1$d 人评丨#%2$d
+ 发送失败:网络错误
+ 发送失败,请附带日志反馈此问题\n%1$s
+ 评论将发送到 Ani,Bangumi 评论为只读
+ 发送评论
+ 渲染中...
+ 回复评论
+ 查看更多 %1$d 条回复>
+ 添加表情
+ 加粗
+ 斜体
+ 下划线
+ 删除线
+ 遮罩
+ 图片
+ 链接
+ 更多评论编辑功能
+ 编辑
+ 预览
+ 发送
+ 无法打开链接,已将链接复制到剪贴板,请打开浏览器访问
+ 此链接可能会打开其他应用,ani 将不会打开此链接:\n
+ 无法打开此链接:\n
+ 此内容不适合展示
+ 临时展示
+ 正在下载 Bangumi 收藏数据
+ 此操作可能需要 5-15 分钟时间,请耐心等待。在下载过程中,你可以正常使用其他功能。可手动刷新收藏列表查看最新进度。
+ 在后台继续
+ 准备中
+ 正在获取元数据
+ (已完成 %1$d 条)正在获取更多收藏列表
+ (已完成 %1$d 条)正在获取观看进度
+ (已完成 %1$d 条)正在保存
+ (已完成 %1$d 条)正在完成
+ (已完成 %1$d 条)同步失败,错误信息如下:\n%2$s
+ (已完成 %1$d 条)同步成功
+ 进行中
+ 没有更多了
+ 上一组
+ 选择分组
+ 下一组
+ #%1$d\n%2$d 人评
+ 复制
+ 错误详情
+ 错误
+ 未匹配到文件
+ 解析超时
+ 不支持的视频类型
+ 未知错误,点击查看
+ 已取消
+ 网络错误,请检查网络连接状况
+ 弹幕加载失败,点击查看
+ %1$d 个弹幕源,共计 %2$d 条弹幕
+ 展示更少
+ 展示更多
+ 弹幕已关闭,可在播放器内开启
+ 弹幕加载中
+ 正在播放:
+ 错误信息
+ 播放失败, 请更换数据源
+ 正在自动选择数据源,请稍候
+ 正在解析资源链接\n通常几秒内完成,否则请切换数据源
+ 资源解析成功, 正在准备视频
+ 正在解析磁力链或查询元数据\n通常几秒内完成, 否则请尝试切换数据源或先缓存再看
+ 正在缓冲
+ BT 初始缓冲耗时稍长, 请耐心等待 30 秒
+ 若持续没有速度, 可尝试切换数据源
+ 缓冲耗时过长, 可尝试切换数据源
+ 加载失败:
+ 解析超时
+ 未知错误
+ 不支持该文件类型
+ 未找到可播放的文件
+ 已取消
+ 网络错误
+ 已想看,可更改为:
+ 选择数据源
+ 关闭选择器
+ 相关推荐
+ %1$s 弹幕时间校准
+ 调整弹幕出现时间以匹配当前视频。正值表示弹幕延后,负值表示弹幕提前。
+ 当前偏移:%1$s
+ 重置为 0
+ 恢复原值
+ 暂无弹幕数据
+ 没有符合筛选条件的弹幕
+ 弹幕列表
+ 收起
+ 展开
+ 更多选项
+ 外部链接
+ 禁用
+ 启用
+ 重新匹配
+ 时间校准 (%1$s)
+ 哔
+ 弹
+ 巴
+ 正在播放
+ 弹幕设置
+ 快进 85 秒
+ 折叠侧边栏
+ 展开侧边栏
+ 预览模式
+ 输入要屏蔽的弹幕关键词(正则)
+ 正则表达式
+ 正则表达式语法不正确。
+ 例如:‘签’ 会屏蔽含文字‘签’的弹幕。
+ 添加
+ 删除
+ 正则弹幕过滤管理
+ 顶部
+ 滚动
+ 底部
+ 彩色
+ 弹幕字号
+ 不透明度
+ 描边宽度
+ 弹幕字重
+ 弹幕速度
+ 弹幕速度不会跟随视频倍速变化
+ 同屏密度
+ 密集
+ 适中
+ 稀疏
+ 显示区域
+ 关闭
+ 1/8 屏
+ 1/6 屏
+ 1/4 屏
+ 半屏
+ 3/4 屏
+ 全屏
+ 启用正则弹幕过滤器
+ 管理正则弹幕过滤器
+ 弹幕调试模式
diff --git a/app/shared/app-lang/src/androidMain/res/values-zh-rHK/strings.xml b/app/shared/app-lang/src/androidMain/res/values-zh-rHK/strings.xml
index 9ecfdb6153..44cfb85b15 100644
--- a/app/shared/app-lang/src/androidMain/res/values-zh-rHK/strings.xml
+++ b/app/shared/app-lang/src/androidMain/res/values-zh-rHK/strings.xml
@@ -96,6 +96,43 @@
步驟 1:搜索條目
步驟 2:搜索劇集
步驟 3:匹配影片
+ 名稱*
+ 設置顯示在列表中的名稱
+ 圖示鏈接
+ 搜索鏈接
+ 示例:https://www.nyacg.net/search.html?wd={keyword}
+ 替換規則:\n{keyword} 替換為條目 (番劇) 名稱
+ Base URL (可選)
+ 可選。用於拼接條目詳情 (劇集列表) 頁面 URL,將會影響步驟 2。默認自動從搜索鏈接生成
+ 僅使用第一個詞
+ 以空格分割,僅使用第一個詞搜索。適用於搜索兼容性差的情況
+ 去除特殊字符
+ 去除特殊字符以及 "電影" 等字樣,提升搜索成功率
+ 嘗試條目名稱數量
+ 每次播放使用多少個條目名稱進行查詢。\n為 1 則只使用主名稱,為 2 額外使用日文原名,大於 2 將額外使用其他別名,別名的數量不固定。\n一般用 1 就夠了,使用多個名稱將會顯著增加播放時的等待時間。
+ 搜索請求間隔時間 (毫秒)
+ 控制每發送一個請求後等待多久後再發送下一個請求
+ 過濾設置
+ 使用條目名稱過濾
+ 要求資源標題包含條目名稱。適用於數據源可能搜到無關內容的情況。此功能只對 4.4.0 以前版本有效,對其他版本無效
+ 使用劇集序號過濾
+ 要求資源標題包含劇集序號。適用於數據源可能搜到無關內容的情況。通常建議開啟
+ 標記分辨率
+ 將此數據源的資源都標記為該分辨率。不影響查詢,只在播放器中選擇數據源時用作偏好和過濾選項。
+ 標記字幕語言
+ 將此數據源的資源都標記為該字幕語言。不影響查詢,只在播放器中選擇數據源時用作偏好和過濾選項。
+ 在播放器內選擇資源時
+ 區分條目名稱
+ 關閉後,所有步驟 1 搜索到的條目都將被視為同一個,它們的相同標題的劇集將會被去重。\n開啟此項則不會這樣去重。\n此選項不影響測試結果,影響播放器內選擇數據源時的結果。
+ 區分線路名稱
+ 關閉後,線路名稱不同,但只要標題相同的劇集就會被去重。\n開啟此項則不會這樣去重。\n此選項不影響測試結果,影響播放器內選擇數據源時的結果。
+ 播放影片時
+ 播放影片時執行的 HTTP 請求的 Referer,可留空
+ 播放影片時執行的 HTTP 請求的 User-Agent
+ 單標籤
+ 多標籤
+ 不區分線路
+ 線路分組
啟用嵌套鏈接
當遇到匹配的鏈接時,終止父頁面加載並跳轉到匹配的鏈接,在嵌套頁面中繼續查找影片鏈接。支持任意次數嵌套
匹配嵌套鏈接
@@ -177,6 +214,11 @@
已關閉,將會跳轉到外部瀏覽器完成下載
自動下載更新
下載完成後會提示,確認後才會安裝
+ 未找到安裝包
+ 打開文件失敗,請手動安裝 %1$s
+ 查看安裝包
+ 取消更新
+ 自動安裝失敗,請手動安裝
檢查中...
檢查失敗
已是最新
@@ -386,6 +428,19 @@
搜索
最高熱度
新番時間表
+ 查看詳情
+ 第 %1$s 話
+ 第 %1$s (%2$s) 話
+ 一
+ 二
+ 三
+ 四
+ 五
+ 六
+ 日
+ 周%1$s
+ 上周%1$s
+ 下周%1$s
繼續觀看
推薦
按住 Shift + 滑鼠滾輪 同樣可以水平滾動
@@ -453,6 +508,10 @@
自定義
自動檢測結果
正在檢測
+ 正在檢測連接,請稍候
+ 部分服務連接失敗,請考慮啟用代理
+ 部分服務連接失敗,請更換代理模式或代理地址
+ 所有服務連接正常
未檢測到系統代理
代理地址
示例: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080
@@ -460,6 +519,15 @@
可選
無
密碼
+ 重新測試
+ 彈幕服務
+ 收藏數據服務
+ 評論服務
+ 連接成功
+ 連接失敗
+ 未啟用代理
+ 正在使用%1$s
+ 保存並測試
anitorrent
@@ -491,6 +559,247 @@
已登出
賬戶
+
+ 版本過期
+ 當前版本過舊,為了你的使用體驗,請更新到最新版本。\n右下角將會顯示最新版本,可以點擊自動更新。如果無法更新,請前往官網下載:
+ 當前版本過舊,為了你的使用體驗,請更新到最新版本 %1$s。\n右下角將會顯示最新版本,可以點擊自動更新。如果無法更新,請前往官網下載:
+ 如果新版本要求導入設置,請點擊:
+ 已複製到剪貼板,請到新版本點擊導入
+ 導出設置
+
+
+ 歡迎使用 Animeko
+ 一站式在線彈幕追番平台 (簡稱 Ani)
+ Ani 目前由愛好者組成的組織 OpenAni 和社區貢獻者維護,完全免費,並在 GitHub 上開源。
+ Ani 的目標是提供盡可能簡單且舒適的追番體驗。
+ 繼續
+ 主題設置
+ 網絡設置
+ 歡迎,%1$s
+ 歡迎
+ 完成
+ 色彩
+ 動態色彩
+ 使用系統強調色
+ Ani 支持邊下邊播 BT 資源,BT 下載速度取決於網絡質量
+ 允許通知權限,在緩存時查看下載進度
+ 啟用 BitTorrent 功能
+ 允許通知
+ 顯示 BT 下載進度和速度等資訊
+ 已授權
+ 授予權限
+ 步驟 %1$d / %2$d
+ 下一步
+ 上一步
+ 跳過
+ 選擇主題
+ 設置代理
+ BitTorrent 功能
+
+
+ 登入
+ 註冊
+ 登入 / 註冊
+ 綁定電子郵件
+ 更改電子郵件
+ 你的電子郵件地址
+ 我們會發送一封驗證碼郵件
+ 電子郵件
+ 清空
+ 繼續
+ 該電子郵件已被使用
+ 驗證碼無效或已過期,請重新發送
+ 輸入驗證碼
+ 請檢查電子郵件 %1$s
+ 正在登入現有賬戶
+ 正在註冊新賬戶
+ 驗證碼
+ 6 位數字
+ 重新發送驗證碼
+ %1$d 秒後可重新發送
+ 其他登入方式
+
+
+ 授權 Bangumi 登入
+ 授權 Bangumi 賬戶後,可以同步你的觀看記錄到 Bangumi,或更方便地登入 Ani。
+ 綁定 Bangumi 賬戶
+ 登入 / 註冊
+ 正在等待結果
+ 已授權
+ 取消
+ 幫助
+ Bangumi 是甚麼
+ 瀏覽器提示網站被屏蔽或禁止訪問
+ 註冊時應該選擇哪一項
+ 註冊或登入時一直提示驗證碼錯誤
+ 無法收到電子郵件驗證碼
+ 註冊時一直激活失敗
+ 其他問題
+ Bangumi 番組計劃是一個中文互聯網的 ACGN 內容分享與交流網站,致力於提供一個輕鬆便捷而獨特的交流環境。\nBangumi 提供番劇索引、番劇收藏和追番進度等功能,Ani 可以將你的觀看記錄同步至 Bangumi。
+ 請在系統設置中更換預設瀏覽器,建議使用 Google Chrome、Microsoft Edge 或 Mozilla Firefox。
+ 選擇「管理 ACG 收藏與收視進度,分享交流」這一項。
+ 如果沒有驗證碼輸入框,可以多點幾次密碼輸入框;如果輸錯了驗證碼,需要重新整理頁面後再登入。
+ 請檢查垃圾郵件箱,並盡量使用常見郵箱註冊,例如 QQ 郵箱、網易郵箱或 Outlook。
+ 刪除激活碼的最後一個字,然後手動輸入刪除的字,或改用其他瀏覽器。
+ 仍然無法解決你的問題?還可以透過以下渠道獲取幫助。
+
+
+ 適應
+ 填充
+ 裁切
+ 自動
+ 音軌
+ 關閉
+ 字幕
+ 禁用彈幕
+ 啟用彈幕
+ 靜音
+ 音量
+ 下一集
+ 選集
+ 發送
+ 倍速
+ 即將跳過 OP 或 ED
+ 取消
+ 來發一條彈幕吧~
+ 小心,我要發射彈幕啦!
+ 每一條彈幕背後,都有一個不為人知的秘密
+ 召喚彈幕精靈!
+ 這一刻的感受,只有你最懂
+ 讓彈幕變得不一樣
+ 彈幕世界大門已開
+ 字裡行間,藏著宇宙的秘密
+ 在光與影的交織中,你的話語是唯一的真實
+ 有趣的靈魂萬里挑一
+ 說點什麼
+ 長期徵集有趣的彈幕廣告詞
+ 廣告位招租
+ 🤔
+ 夢開始的地方
+ 心念成形
+ 發個彈幕炒熱氣氛!
+ 來個彈幕吧!
+ 發個友善的彈幕吧!
+ 是不是忍不住想發彈幕了呢?
+
+
+ 想看
+ 在看
+ 看過
+ 擱置
+ 拋棄
+ 取消追番
+ 追番
+ 已想看
+ 已在看
+ 已看過
+ 已擱置
+ 已拋棄
+ 未追番
+ 取消追番
+ 這將會清除你的觀看進度和評價。此操作無法撤銷。確定要取消追番嗎?
+ 刪除
+ 取消
+ 要同時將所有劇集設為看過嗎?
+ 設置
+ 忽略
+ 正在同步
+ 追番
+ 移至"看過"
+ 未收藏
+ 遊客模式下請先搜尋後觀看,或登入後使用收藏功能
+ %1$s 收藏 / %2$s 在看
+ / %1$s 拋棄
+ %1$s 年 %2$s 月
+
+
+ 取消看過
+ 已看過
+ 已拋棄
+ 看過
+ 選集播放
+ 長按還可以標記為已看
+ 條目詳情
+ 關閉
+ 緩存
+ 收藏狀態
+ 緩存類型
+ 下載狀態
+ 下載中
+ 已完成
+ 排序
+ 最新下載
+ 最早下載
+ 條目名 A-Z
+ 條目名 Z-A
+ 劇集升序
+ 劇集降序
+ 想看
+ 在看
+ 看過
+ 擱置
+ 拋棄
+ 未收藏
+ 總上傳
+ 總下載
+ 封面
+ 繼續下載
+ 暫停下載
+ 管理此項
+ 下載完成
+ 下載失敗
+ 下載中
+ 單集緩存
+ 刪除
+ 取消
+ 緩存
+ 選擇儲存位置
+ 管理全部緩存
+ 未知
+ 選擇一個條目查看具體緩存
+ %1$d 個已選
+ 退出選擇
+ 選擇所有
+ 刪除所選
+ 進入選擇模式
+ 刪除緩存
+ 刪除後不可恢復,確認刪除嗎?
+ %1$d/%2$d 已完成
+ %1$d 個下載中
+ 第%1$d話 · %2$s
+ 更多操作
+ 播放
+ 緩存資訊無效,無法播放
+ 此資源不支援邊下邊播,請等待下載完成
+ 更多資訊
+ 詳情
+ 已複製
+ 複製
+ 打開連結
+ 打開檔案失敗:%1$s
+ 瀏覽檔案
+ 劇集範圍
+ 數據源
+ 在線
+ 本地
+ 字幕組
+ 字幕語言
+ 發佈時間
+ 解析度
+ 檔案大小
+ 原始連結
+ 檔案類型
+ 原始下載連結
+ 本地緩存路徑
+ 總片段數
+ 下載器內部狀態
+ 外掛字幕 %1$d
+ 未登入
+ 編輯個人資料
+ 登入 / 註冊
+ 設置
+ 退出登入
+ 確定要退出登入嗎?
Bangumi 登入過期
Bangumi 登入過期,請重新登入
@@ -506,14 +815,389 @@
確定要登出嗎?
登出
取消
-
-
- 版本過期
- 當前版本過舊,為了你的使用體驗,請更新到最新版本。\n右下角將會顯示最新版本,可以點擊自動更新。如果無法更新,請前往官網下載:
- 當前版本過舊,為了你的使用體驗,請更新到最新版本 %1$s。\n右下角將會顯示最新版本,可以點擊自動更新。如果無法更新,請前往官網下載:
- 如果新版本要求導入設置,請點擊:
- 已複製到剪貼板,請到新版本點擊導入
- 導出設置
+ 手動全量同步
+ 重新下載全部 Bangumi 資料
+ 將 Bangumi 的收藏資料下載到 Animeko 收藏服務。通常不需要進行這個操作,Animeko 能自動完成同步。僅在你發現資料不一致時才需要手動下載。此操作可能需要數分鐘才能完成,在同步過程中其他功能不可用。請注意,每十分鐘只能執行一次全量同步
+ 同步佇列
+ 待執行的同步操作
+ 執行全部
+ 載入中...
+ 載入中載入中載入中載入中...
+ 更新收藏:%1$d (%2$s)
+ 刪除收藏:%1$d
+ 標記劇集為未看過:%1$d
+ 標記劇集為看過:%1$d (%2$s)
+ 未知操作(請更新版本)
+ 暱稱
+ 未設置
+ 最多 20 字,只能包含中文、日文、英文、數字和底線
+ 電子郵件
+ 綁定
+ 用戶 ID
+ 第三方帳號
+ 未綁定
+ 解除綁定
+ 確定要解除綁定 Bangumi 嗎?解除綁定後將不再同步觀看記錄到 Bangumi。之後可以重新綁定。
+ 選擇頭像
+ 完成
+ 上傳頭像
+ 選擇檔案
+ 支援 JPEG/PNG/WebP,最大 1MB。多次上傳需間隔一分鐘。
+ 或拖動檔案到此處。支援 JPEG/PNG/WebP,最大 1MB。多次上傳需間隔一分鐘。
+ 正在上傳...
+ 圖片大小超過 1MB
+ 不支援的圖片格式
+ 裁剪並上傳
+ 裁剪頭像
+ 拖動選框移動,拖動角點調整大小
+ 將番劇收藏為 "%1$s" 後將在這裡顯示
+ 登入後可收藏
+ 即將上線,敬請期待
+ 寫評價
+ 詳情
+ 評價
+ 討論
+ 角色
+ 角色 %1$d
+ 製作人員
+ 製作人員 %1$d
+ 關聯條目
+ 顯示更少
+ 顯示更多
+ 查看全部
+ 前傳
+ 續集
+ 衍生
+ 番外篇
+ 網絡錯誤
+ 操作過快,請重試
+ 服務暫不可用
+ 無搜尋結果
+ 此功能需要登入
+ 未知錯誤:%1$s
+ 無詳細資訊
+ 請求錯誤:%1$s
+ 已複製,請回報到 GitHub issues 或群組裡
+ 發生未知錯誤,請在設置中回報(附加日誌)
+ 操作失敗,請重試
+ 操作失敗
+ 內嵌
+ 內封
+ 外掛
+ 未知
+ 內封或未知
+ 粵語
+ 簡中
+ 繁中
+ 日語
+ 英語
+ 解析度
+ 字幕
+ 字幕組
+ 展開
+ 清除篩選
+ 目前選取
+ 無字幕
+ 單集資源
+ 不支援播放
+ 季度不相符
+ 條目標題不相符
+ 請求無效,請檢查
+ 儲存並重新整理
+ 編輯查詢請求
+ 主要搜尋名稱
+ 大多數資料來源只使用此名稱
+ 次要搜尋名稱
+ 線上來源會忽略這些名稱
+ 收起
+ 展開
+ 刪除名稱 %1$d
+ 新增名稱
+ 劇集資訊
+ 資源必須至少匹配以下兩種資訊中的一種,否則不會顯示。可以只修改其中一種
+ 系列內劇集序號
+ 假設有兩季,分別有 12 集,則第二季的第一集為 13
+ 條目內序號
+ 在目前季度內的序號,例如第二季的第一集為 01
+ 捨棄
+ 繼續編輯
+ 有未儲存的編輯,要捨棄編輯嗎?
+ 都不對?
+ 修改查詢
+ 正在等待驗證碼處理
+ 手動選擇
+ 更換
+ 正在自動選擇資料來源
+ 請選擇資料來源
+ 資料來源
+ 已查找:
+ 正在查詢
+ 已查詢
+ %1$s %2$d/%3$d 個資料來源
+ 說明
+ 設定
+ 點擊暫時啟用
+ 查詢失敗
+ 點擊重試
+ 需要驗證碼
+ 點擊驗證
+ 查詢成功
+ 驗證
+ 資料來源說明
+ 資料來源類型
+ 從 BitTorrent 網絡獲取資源,清晰度高,資源全面,載入速度可能不快
+ 從線上影片網站獲取資源,載入速度快,但清晰度通常不高
+ 簡單模式
+ 詳細模式
+ 篩選到 %1$d/%2$d 條資源
+ 顯示已被排除的資源 (%1$d)
+ 無法打開連結
+ 打開原始連結 %1$s
+ 編輯配置
+ 根據步驟 3 的配置,從 %1$d 個連結中未匹配到播放連結,請檢查配置
+ 根據步驟 3 的配置,從 %1$d 個連結中匹配到了 %2$d 個播放連結
+ 根據步驟 3 的配置,從 %1$d 個連結中匹配到了 %2$d 個播放連結。為了更好的穩定性,建議調整規則,匹配到正好一個連結
+ 隱藏圖片
+ 隱藏 CSS/字體
+ 隱藏 JS/WASM
+ 隱藏 data
+ 將實際播放:%1$s
+ 巢狀連結
+ 匹配
+ 未匹配
+ 第 %1$s 話
+ 已快取
+ 彈幕數量
+ 已停用
+ 設定 %1$s
+ 嗶哩嗶哩
+ 彈彈
+ 精確匹配
+ 半模糊匹配
+ 模糊匹配
+ 無匹配
+ 影片串流連結
+ 種子檔案下載連結
+ 本機檔案連結
+ 磁力連結
+ 網頁連結
+ 複製%1$s
+ 訪問%1$s
+ 用其他應用打開
+ 複製資料來源頁面連結
+ 訪問資料來源頁面
+ 劇集列表
+ 查看更多劇集
+ 繼續觀看 %1$s
+ 已看完
+ 還未開播
+ %1$s開播
+ 開始觀看
+ %1$s更新
+ 看過 %1$s
+ 未知
+ 未開播
+ 連載中
+ 連載至 %1$s
+ 已完結
+ 全 %1$s 話
+ 預定全 %1$s 話
+ 評論
+ 評論 %1$d
+ 分享
+ 下載
+ 更換彈幕
+ 正在查詢劇集列表…
+ 正在查詢彈幕列表…
+ 選擇條目
+ 選擇劇集
+ 發送彈幕
+ 回到頂部
+ 已顯示 %1$d 個結果
+ 最佳匹配
+ 最多收藏
+ 最高排名
+ 發布日期
+ 製作:
+ 受眾
+ 分類
+ 角色
+ 情感
+ 類型
+ 分級
+ 地區
+ 系列
+ 設定
+ 來源
+ 技術
+ 自定義
+ 背景運行
+ 快取功能需要應用保持在背景運行才能下載影片
+ 停用電池最佳化
+ 可以幫助保持在背景運行。可能增加耗電
+ 通知設定
+ 開啟設定
+ 彈幕刷新率
+ 分享當日日誌檔案
+ 分享日誌檔案
+ 複製當日日誌內容(很大)
+ 未找到日誌檔案
+ 請先收藏再評分
+ 捨棄編輯
+ 評價尚未保存,確定要捨棄嗎?
+ 捨棄
+ 修改評分
+ 評價
+ 說點什麼...
+ 可留空
+ 僅自己可見
+ 不忍直視(請謹慎評價)
+ 很差
+ 差
+ 較差
+ 不過不失
+ 還行
+ 推薦
+ 力薦
+ 神作
+ 超神作(請謹慎評價)
+ 你的評分: %1$d
+ %1$d 人評丨#%2$d
+ 發送失敗:網絡錯誤
+ 發送失敗,請附帶日誌反饋此問題\n%1$s
+ 評論將發送到 Ani,Bangumi 評論為唯讀
+ 發送評論
+ 渲染中...
+ 回覆評論
+ 查看更多 %1$d 條回覆>
+ 添加表情
+ 粗體
+ 斜體
+ 下劃線
+ 刪除線
+ 遮罩
+ 圖片
+ 連結
+ 更多評論編輯功能
+ 編輯
+ 預覽
+ 發送
+ 無法打開連結,已將連結複製到剪貼簿,請打開瀏覽器訪問
+ 此連結可能會打開其他應用,ani 將不會打開此連結:\n
+ 無法打開此連結:\n
+ 此內容不適合展示
+ 臨時展示
+ 正在下載 Bangumi 收藏資料
+ 此操作可能需要 5-15 分鐘時間,請耐心等待。在下載過程中,你可以正常使用其他功能。可手動刷新收藏列表查看最新進度。
+ 在背景繼續
+ 準備中
+ 正在取得元資料
+ (已完成 %1$d 條)正在取得更多收藏列表
+ (已完成 %1$d 條)正在取得觀看進度
+ (已完成 %1$d 條)正在保存
+ (已完成 %1$d 條)正在完成
+ (已完成 %1$d 條)同步失敗,錯誤資訊如下:\n%2$s
+ (已完成 %1$d 條)同步成功
+ 進行中
+ 沒有更多了
+ 上一組
+ 選擇分組
+ 下一組
+ #%1$d\n%2$d 人評
+ 複製
+ 錯誤詳情
+ 錯誤
+ 未匹配到檔案
+ 解析超時
+ 不支援的影片類型
+ 未知錯誤,點擊查看
+ 已取消
+ 網絡錯誤,請檢查網絡連接狀況
+ 彈幕載入失敗,點擊查看
+ %1$d 個彈幕源,共計 %2$d 條彈幕
+ 顯示更少
+ 顯示更多
+ 彈幕已關閉,可在播放器內開啟
+ 彈幕載入中
+ 正在播放:
+ 錯誤資訊
+ 播放失敗, 請更換數據源
+ 正在自動選擇數據源,請稍候
+ 正在解析資源連結\n通常幾秒內完成,否則請切換數據源
+ 資源解析成功, 正在準備影片
+ 正在解析磁力鏈或查詢元數據\n通常幾秒內完成, 否則請嘗試切換數據源或先快取再看
+ 正在緩衝
+ BT 初始緩衝耗時稍長, 請耐心等待 30 秒
+ 若持續沒有速度, 可嘗試切換數據源
+ 緩衝耗時過長, 可嘗試切換數據源
+ 加載失敗:
+ 解析超時
+ 未知錯誤
+ 不支援該檔案類型
+ 未找到可播放的檔案
+ 已取消
+ 網絡錯誤
+ 已想看,可更改為:
+ 選擇數據源
+ 關閉選擇器
+ 相關推薦
+ %1$s 彈幕時間校準
+ 調整彈幕出現時間以匹配當前影片。正值表示彈幕延後,負值表示彈幕提前。
+ 當前偏移:%1$s
+ 重設為 0
+ 恢復原值
+ 暫無彈幕資料
+ 沒有符合篩選條件的彈幕
+ 彈幕列表
+ 收起
+ 展開
+ 更多選項
+ 外部連結
+ 停用
+ 啟用
+ 重新匹配
+ 時間校準 (%1$s)
+ 嗶
+ 彈
+ 巴
+ 正在播放
+ 彈幕設置
+ 快進 85 秒
+ 摺疊側邊欄
+ 展開側邊欄
+ 預覽模式
+ 輸入要屏蔽的彈幕關鍵詞(正則)
+ 正則表達式
+ 正則表達式語法不正確。
+ 例如:‘簽’ 會屏蔽含文字‘簽’的彈幕。
+ 添加
+ 刪除
+ 正則彈幕過濾管理
+ 頂部
+ 滾動
+ 底部
+ 彩色
+ 彈幕字號
+ 不透明度
+ 描邊寬度
+ 彈幕字重
+ 彈幕速度
+ 彈幕速度不會跟隨影片倍速變化
+ 同屏密度
+ 密集
+ 適中
+ 稀疏
+ 顯示區域
+ 關閉
+ 1/8 屏
+ 1/6 屏
+ 1/4 屏
+ 半屏
+ 3/4 屏
+ 全屏
+ 啟用正則彈幕過濾器
+ 管理正則彈幕過濾器
+ 彈幕調試模式
diff --git a/app/shared/app-lang/src/androidMain/res/values-zh-rTW/strings.xml b/app/shared/app-lang/src/androidMain/res/values-zh-rTW/strings.xml
index 7a11cb1098..1a030c7537 100644
--- a/app/shared/app-lang/src/androidMain/res/values-zh-rTW/strings.xml
+++ b/app/shared/app-lang/src/androidMain/res/values-zh-rTW/strings.xml
@@ -96,6 +96,43 @@
步驟 1:搜尋條目
步驟 2:搜尋劇集
步驟 3:匹配影片
+ 名稱*
+ 設定顯示在列表中的名稱
+ 圖示連結
+ 搜尋連結
+ 範例:https://www.nyacg.net/search.html?wd={keyword}
+ 替換規則:\n{keyword} 替換為條目 (番劇) 名稱
+ Base URL (可選)
+ 可選。用於拼接條目詳情 (劇集列表) 頁面 URL,將會影響步驟 2。預設自動從搜尋連結生成
+ 僅使用第一個詞
+ 以空格分割,僅使用第一個詞搜尋。適用於搜尋相容性差的情況
+ 去除特殊字元
+ 去除特殊字元以及 "電影" 等字樣,提升搜尋成功率
+ 嘗試條目名稱數量
+ 每次播放使用多少個條目名稱進行查詢。\n為 1 則只使用主名稱,為 2 額外使用日文原名,大於 2 將額外使用其他別名,別名的數量不固定。\n一般用 1 就夠了,使用多個名稱將會顯著增加播放時的等待時間。
+ 搜尋請求間隔時間 (毫秒)
+ 控制每送出一個請求後等待多久再送出下一個請求
+ 過濾設定
+ 使用條目名稱過濾
+ 要求資源標題包含條目名稱。適用於資料來源可能搜到無關內容的情況。此功能只對 4.4.0 以前版本有效,對其他版本無效
+ 使用劇集序號過濾
+ 要求資源標題包含劇集序號。適用於資料來源可能搜到無關內容的情況。通常建議開啟
+ 標記解析度
+ 將此資料來源的資源都標記為該解析度。不影響查詢,只在播放器中選擇資料來源時用作偏好和過濾選項。
+ 標記字幕語言
+ 將此資料來源的資源都標記為該字幕語言。不影響查詢,只在播放器中選擇資料來源時用作偏好和過濾選項。
+ 在播放器內選擇資源時
+ 區分條目名稱
+ 關閉後,所有步驟 1 搜尋到的條目都將被視為同一個,它們的相同標題的劇集將會被去重。\n開啟此項則不會這樣去重。\n此選項不影響測試結果,影響播放器內選擇資料來源時的結果。
+ 區分線路名稱
+ 關閉後,線路名稱不同,但只要標題相同的劇集就會被去重。\n開啟此項則不會這樣去重。\n此選項不影響測試結果,影響播放器內選擇資料來源時的結果。
+ 播放影片時
+ 播放影片時執行的 HTTP 請求 Referer,可留空
+ 播放影片時執行的 HTTP 請求 User-Agent
+ 單標籤
+ 多標籤
+ 不區分線路
+ 線路分組
啟用巢狀連結
當遇到匹配的連結時,終止父頁面載入並跳轉到匹配的連結,在巢狀頁面中繼續尋找影片連結。支援任意次數的巢狀
匹配巢狀連結
@@ -177,6 +214,11 @@
已關閉,將會跳轉到外部瀏覽器完成下載
自動下載更新
下載完成後會提示,確認後才會安裝
+ 未找到安裝包
+ 打開檔案失敗,請手動安裝 %1$s
+ 查看安裝包
+ 取消更新
+ 自動安裝失敗,請手動安裝
檢查中…
檢查失敗
已是最新
@@ -386,6 +428,19 @@
搜尋
最高熱度
新番時間表
+ 查看詳情
+ 第 %1$s 話
+ 第 %1$s (%2$s) 話
+ 一
+ 二
+ 三
+ 四
+ 五
+ 六
+ 日
+ 週%1$s
+ 上週%1$s
+ 下週%1$s
繼續觀看
推薦
按住 Shift + 滑鼠滾輪 同樣可以水平滾動
@@ -453,6 +508,10 @@
自訂
自動偵測結果
正在偵測
+ 正在檢測連線,請稍候
+ 部分服務連線失敗,請考慮啟用代理
+ 部分服務連線失敗,請更換代理模式或代理地址
+ 所有服務連線正常
未偵測到系統代理
代理地址
範例: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080
@@ -460,6 +519,15 @@
可選
無
密碼
+ 重新測試
+ 彈幕服務
+ 收藏資料服務
+ 評論服務
+ 連線成功
+ 連線失敗
+ 未啟用代理
+ 正在使用%1$s
+ 儲存並測試
anitorrent
@@ -486,10 +554,241 @@
僅快取最近看過的番劇
快取數量
目前設定: 僅快取最近看過的 %d 部番劇
- 管理已快取的劇集
- 登出
- 已登出
- 賬戶
+ 管理已快取的劇集登出已登出賬戶
+
+
+ 歡迎使用 Animeko
+ 一站式線上彈幕追番平台 (簡稱 Ani)
+ Ani 目前由愛好者組成的組織 OpenAni 和社群貢獻者維護,完全免費,並在 GitHub 上開源。
+ Ani 的目標是提供盡可能簡單且舒適的追番體驗。
+ 繼續
+ 主題設定
+ 網路設定
+ 歡迎,%1$s
+ 歡迎
+ 完成
+ 色彩
+ 動態色彩
+ 使用系統強調色
+ Ani 支援邊下邊播 BT 資源,BT 下載速度取決於網路品質
+ 允許通知權限,以便在快取時查看下載進度
+ 啟用 BitTorrent 功能
+ 允許通知
+ 顯示 BT 下載進度和速度等資訊
+ 已授權
+ 授予權限
+ 步驟 %1$d / %2$d
+ 下一步
+ 上一步
+ 跳過
+ 選擇主題
+ 設定代理
+ BitTorrent 功能
+
+
+ 登入
+ 註冊
+ 登入 / 註冊
+ 綁定電子郵件
+ 更改電子郵件
+ 你的電子郵件地址
+ 我們會寄送一封驗證碼郵件
+ 電子郵件
+ 清空
+ 繼續
+ 該電子郵件已被使用
+ 驗證碼無效或已過期,請重新發送
+ 輸入驗證碼
+ 請檢查電子郵件 %1$s
+ 正在登入現有帳號
+ 正在註冊新帳號
+ 驗證碼
+ 6 位數字
+ 重新發送驗證碼
+ %1$d 秒後可重新發送
+ 其他登入方式
+
+
+ 授權 Bangumi 登入
+ 授權 Bangumi 帳號後,可以同步你的觀看紀錄到 Bangumi,或更方便地登入 Ani。
+ 綁定 Bangumi 帳號
+ 登入 / 註冊
+ 正在等待結果
+ 已授權
+ 取消
+ 幫助
+ Bangumi 是什麼
+ 瀏覽器提示網站被封鎖或禁止存取
+ 註冊時應該選擇哪一項
+ 註冊或登入時一直提示驗證碼錯誤
+ 無法收到電子郵件驗證碼
+ 註冊時一直啟用失敗
+ 其他問題
+ Bangumi 番組計劃是一個中文網際網路的 ACGN 內容分享與交流網站,致力於提供一個輕鬆便捷又獨特的交流環境。\nBangumi 提供番劇索引、番劇收藏和追番進度等功能,Ani 可以將你的觀看紀錄同步到 Bangumi。
+ 請在系統設定中更換預設瀏覽器,建議使用 Google Chrome、Microsoft Edge 或 Mozilla Firefox。
+ 選擇「管理 ACG 收藏與收視進度,分享交流」這一項。
+ 如果沒有驗證碼輸入框,可以多點幾次密碼輸入框;如果輸錯了驗證碼,需要重新整理頁面後再登入。
+ 請檢查垃圾郵件匣,並盡量使用常見信箱註冊,例如 QQ 郵箱、網易郵箱或 Outlook。
+ 刪除啟用碼的最後一個字,然後手動輸入刪掉的字,或改用其他瀏覽器。
+ 仍然無法解決你的問題?還可以透過以下管道取得幫助。
+
+
+ 適應
+ 填充
+ 裁切
+ 自動
+ 音軌
+ 關閉
+ 字幕
+ 停用彈幕
+ 啟用彈幕
+ 靜音
+ 音量
+ 下一集
+ 選集
+ 傳送
+ 倍速
+ 即將跳過 OP 或 ED
+ 取消
+ 來發一條彈幕吧~
+ 小心,我要發射彈幕啦!
+ 每一條彈幕背後,都有一個不為人知的秘密
+ 召喚彈幕精靈!
+ 這一刻的感受,只有你最懂
+ 讓彈幕變得不一樣
+ 彈幕世界大門已開
+ 字裡行間,藏著宇宙的秘密
+ 在光與影的交織中,你的話語是唯一的真實
+ 有趣的靈魂萬裡挑一
+ 說點什麼
+ 長期徵集有趣的彈幕廣告詞
+ 廣告位招租
+ 🤔
+ 夢開始的地方
+ 心念成形
+ 發個彈幕炒熱氣氛!
+ 來個彈幕吧!
+ 發個友善的彈幕吧!
+ 是不是忍不住想發彈幕了呢?
+
+
+ 想看
+ 在看
+ 看過
+ 擱置
+ 拋棄
+ 取消追番
+ 追番
+ 已想看
+ 已在看
+ 已看過
+ 已擱置
+ 已拋棄
+ 未追番
+ 取消追番
+ 這將會清除你的觀看進度和評價。此操作無法撤銷。確定要取消追番嗎?
+ 刪除
+ 取消
+ 要同時將所有劇集設為看過嗎?
+ 設定
+ 忽略
+ 正在同步
+ 追番
+ 移至"看過"
+ 未收藏
+ 遊客模式下請先搜尋後觀看,或登入後使用收藏功能
+ %1$s 收藏 / %2$s 在看
+ / %1$s 拋棄
+ %1$s 年 %2$s 月
+
+
+ 取消看過
+ 已看過
+ 已拋棄
+ 看過
+ 選集播放
+ 長按還可以標記為已看
+ 條目詳情
+ 關閉
+ 快取
+ 收藏狀態
+ 緩存類型
+ 下載狀態
+ 下載中
+ 已完成
+ 排序
+ 最新下載
+ 最早下載
+ 條目名 A-Z
+ 條目名 Z-A
+ 劇集升序
+ 劇集降序
+ 想看
+ 在看
+ 看過
+ 擱置
+ 拋棄
+ 未收藏
+ 總上傳
+ 總下載
+ 封面
+ 繼續下載
+ 暫停下載
+ 管理此項
+ 下載完成
+ 下載失敗
+ 下載中
+ 單集緩存
+ 刪除
+ 取消
+ 緩存
+ 選擇儲存位置
+ 管理全部緩存
+ 未知
+ 選擇一個條目查看具體緩存
+ %1$d 個已選
+ 退出選擇
+ 選擇所有
+ 刪除所選
+ 進入選擇模式
+ 刪除緩存
+ 刪除後不可恢復,確認刪除嗎?
+ %1$d/%2$d 已完成
+ %1$d 個下載中
+ 第%1$d話 · %2$s
+ 更多操作
+ 播放
+ 緩存資訊無效,無法播放
+ 此資源不支援邊下邊播,請等待下載完成
+ 更多資訊
+ 詳情
+ 已複製
+ 複製
+ 打開連結
+ 打開檔案失敗:%1$s
+ 瀏覽檔案
+ 劇集範圍
+ 數據源
+ 在線
+ 本地
+ 字幕組
+ 字幕語言
+ 發佈時間
+ 解析度
+ 檔案大小
+ 原始連結
+ 檔案類型
+ 原始下載連結
+ 本地緩存路徑
+ 總片段數
+ 下載器內部狀態
+ 外掛字幕 %1$d
+ 未登入
+ 編輯個人資料
+ 登入 / 註冊
+ 設定
+ 退出登入
+ 確定要退出登入嗎?
Bangumi 登入過期
@@ -506,6 +805,389 @@
確定要登出嗎?
登出
取消
+ 手動全量同步
+ 重新下載全部 Bangumi 資料
+ 將 Bangumi 的收藏資料下載到 Animeko 收藏服務。通常不需要進行這個操作,Animeko 能自動完成同步。僅在你發現資料不一致時才需要手動下載。此操作可能需要數分鐘才能完成,在同步過程中其他功能不可用。請注意,每十分鐘只能執行一次全量同步
+ 同步佇列
+ 待執行的同步操作
+ 執行全部
+ 載入中...
+ 載入中載入中載入中載入中...
+ 更新收藏:%1$d (%2$s)
+ 刪除收藏:%1$d
+ 標記劇集為未看過:%1$d
+ 標記劇集為看過:%1$d (%2$s)
+ 未知操作(請更新版本)
+ 暱稱
+ 未設定
+ 最多 20 字,只能包含中文、日文、英文、數字和底線
+ 電子郵件
+ 綁定
+ 使用者 ID
+ 第三方帳號
+ 未綁定
+ 解除綁定
+ 確定要解除綁定 Bangumi 嗎?解除綁定後將不再同步觀看紀錄到 Bangumi。之後可以重新綁定。
+ 選擇頭像
+ 完成
+ 上傳頭像
+ 選擇檔案
+ 支援 JPEG/PNG/WebP,最大 1MB。多次上傳需間隔一分鐘。
+ 或拖動檔案到此處。支援 JPEG/PNG/WebP,最大 1MB。多次上傳需間隔一分鐘。
+ 正在上傳...
+ 圖片大小超過 1MB
+ 不支援的圖片格式
+ 裁剪並上傳
+ 裁剪頭像
+ 拖動選框移動,拖動角點調整大小
+ 將番劇收藏為 "%1$s" 後將在這裡顯示
+ 登入後可收藏
+ 即將上線,敬請期待
+ 寫評價
+ 詳情
+ 評價
+ 討論
+ 角色
+ 角色 %1$d
+ 製作人員
+ 製作人員 %1$d
+ 關聯條目
+ 顯示更少
+ 顯示更多
+ 查看全部
+ 前傳
+ 續集
+ 衍生
+ 番外篇
+ 網路錯誤
+ 操作過快,請重試
+ 服務暫不可用
+ 無搜尋結果
+ 此功能需要登入
+ 未知錯誤:%1$s
+ 無詳細資訊
+ 請求錯誤:%1$s
+ 已複製,請回報到 GitHub issues 或群組裡
+ 發生未知錯誤,請在設定中回報(附加日誌)
+ 操作失敗,請重試
+ 操作失敗
+ 內嵌
+ 內封
+ 外掛
+ 未知
+ 內封或未知
+ 粵語
+ 簡中
+ 繁中
+ 日語
+ 英語
+ 解析度
+ 字幕
+ 字幕組
+ 展開
+ 清除篩選
+ 目前選取
+ 無字幕
+ 單集資源
+ 不支援播放
+ 季度不相符
+ 條目標題不相符
+ 請求無效,請檢查
+ 儲存並重新整理
+ 編輯查詢請求
+ 主要搜尋名稱
+ 大多數資料來源只使用此名稱
+ 次要搜尋名稱
+ 線上來源會忽略這些名稱
+ 收起
+ 展開
+ 刪除名稱 %1$d
+ 新增名稱
+ 劇集資訊
+ 資源必須至少匹配以下兩種資訊中的一種,否則不會顯示。可以只修改其中一種
+ 系列內劇集序號
+ 假設有兩季,分別有 12 集,則第二季的第一集為 13
+ 條目內序號
+ 在目前季度內的序號,例如第二季的第一集為 01
+ 捨棄
+ 繼續編輯
+ 有未儲存的編輯,要捨棄編輯嗎?
+ 都不對?
+ 修改查詢
+ 正在等待驗證碼處理
+ 手動選擇
+ 更換
+ 正在自動選擇資料來源
+ 請選擇資料來源
+ 資料來源
+ 已查找:
+ 正在查詢
+ 已查詢
+ %1$s %2$d/%3$d 個資料來源
+ 說明
+ 設定
+ 點擊暫時啟用
+ 查詢失敗
+ 點擊重試
+ 需要驗證碼
+ 點擊驗證
+ 查詢成功
+ 驗證
+ 資料來源說明
+ 資料來源類型
+ 從 BitTorrent 網路獲取資源,清晰度高,資源全面,載入速度可能不快
+ 從線上影片網站獲取資源,載入速度快,但清晰度通常不高
+ 簡單模式
+ 詳細模式
+ 篩選到 %1$d/%2$d 條資源
+ 顯示已被排除的資源 (%1$d)
+ 無法打開連結
+ 打開原始連結 %1$s
+ 編輯配置
+ 根據步驟 3 的配置,從 %1$d 個連結中未匹配到播放連結,請檢查配置
+ 根據步驟 3 的配置,從 %1$d 個連結中匹配到了 %2$d 個播放連結
+ 根據步驟 3 的配置,從 %1$d 個連結中匹配到了 %2$d 個播放連結。為了更好的穩定性,建議調整規則,匹配到正好一個連結
+ 隱藏圖片
+ 隱藏 CSS/字體
+ 隱藏 JS/WASM
+ 隱藏 data
+ 將實際播放:%1$s
+ 巢狀連結
+ 匹配
+ 未匹配
+ 第 %1$s 話
+ 已快取
+ 彈幕數量
+ 已停用
+ 設定 %1$s
+ 嗶哩嗶哩
+ 彈彈
+ 精確匹配
+ 半模糊匹配
+ 模糊匹配
+ 無匹配
+ 影片串流連結
+ 種子檔案下載連結
+ 本機檔案連結
+ 磁力連結
+ 網頁連結
+ 複製%1$s
+ 訪問%1$s
+ 用其他應用打開
+ 複製資料來源頁面連結
+ 訪問資料來源頁面
+ 劇集列表
+ 查看更多劇集
+ 繼續觀看 %1$s
+ 已看完
+ 還未開播
+ %1$s開播
+ 開始觀看
+ %1$s更新
+ 看過 %1$s
+ 未知
+ 未開播
+ 連載中
+ 連載至 %1$s
+ 已完結
+ 全 %1$s 話
+ 預定全 %1$s 話
+ 評論
+ 評論 %1$d
+ 分享
+ 下載
+ 更換彈幕
+ 正在查詢劇集列表…
+ 正在查詢彈幕列表…
+ 選擇條目
+ 選擇劇集
+ 發送彈幕
+ 回到頂部
+ 已顯示 %1$d 個結果
+ 最佳匹配
+ 最多收藏
+ 最高排名
+ 發布日期
+ 製作:
+ 受眾
+ 分類
+ 角色
+ 情感
+ 類型
+ 分級
+ 地區
+ 系列
+ 設定
+ 來源
+ 技術
+ 自訂
+ 背景運行
+ 快取功能需要應用保持在背景運行才能下載影片
+ 停用電池最佳化
+ 可以幫助保持在背景運行。可能增加耗電
+ 通知設定
+ 開啟設定
+ 彈幕刷新率
+ 分享當日日誌檔案
+ 分享日誌檔案
+ 複製當日日誌內容(很大)
+ 未找到日誌檔案
+ 請先收藏再評分
+ 捨棄編輯
+ 評價尚未保存,確定要捨棄嗎?
+ 捨棄
+ 修改評分
+ 評價
+ 說點什麼...
+ 可留空
+ 僅自己可見
+ 不忍直視(請謹慎評價)
+ 很差
+ 差
+ 較差
+ 不過不失
+ 還行
+ 推薦
+ 力薦
+ 神作
+ 超神作(請謹慎評價)
+ 你的評分: %1$d
+ %1$d 人評丨#%2$d
+ 發送失敗:網路錯誤
+ 發送失敗,請附帶日誌反饋此問題\n%1$s
+ 評論將發送到 Ani,Bangumi 評論為唯讀
+ 發送評論
+ 渲染中...
+ 回覆評論
+ 查看更多 %1$d 則回覆>
+ 添加表情
+ 粗體
+ 斜體
+ 下劃線
+ 刪除線
+ 遮罩
+ 圖片
+ 連結
+ 更多評論編輯功能
+ 編輯
+ 預覽
+ 發送
+ 無法打開連結,已將連結複製到剪貼簿,請打開瀏覽器訪問
+ 此連結可能會打開其他應用,ani 將不會打開此連結:\n
+ 無法打開此連結:\n
+ 此內容不適合展示
+ 臨時展示
+ 正在下載 Bangumi 收藏資料
+ 此操作可能需要 5-15 分鐘時間,請耐心等待。在下載過程中,你可以正常使用其他功能。可手動重新整理收藏列表查看最新進度。
+ 在背景繼續
+ 準備中
+ 正在取得中繼資料
+ (已完成 %1$d 條)正在取得更多收藏列表
+ (已完成 %1$d 條)正在取得觀看進度
+ (已完成 %1$d 條)正在保存
+ (已完成 %1$d 條)正在完成
+ (已完成 %1$d 條)同步失敗,錯誤資訊如下:\n%2$s
+ (已完成 %1$d 條)同步成功
+ 進行中
+ 沒有更多了
+ 上一組
+ 選擇分組
+ 下一組
+ #%1$d\n%2$d 人評
+ 複製
+ 錯誤詳情
+ 錯誤
+ 未匹配到檔案
+ 解析逾時
+ 不支援的影片類型
+ 未知錯誤,點擊查看
+ 已取消
+ 網路錯誤,請檢查網路連線狀況
+ 彈幕載入失敗,點擊查看
+ %1$d 個彈幕源,共計 %2$d 條彈幕
+ 顯示更少
+ 顯示更多
+ 彈幕已關閉,可在播放器內開啟
+ 彈幕載入中
+ 正在播放:
+ 錯誤資訊
+ 播放失敗, 請更換資料來源
+ 正在自動選擇資料來源,請稍候
+ 正在解析資源連結\n通常幾秒內完成,否則請切換資料來源
+ 資源解析成功, 正在準備影片
+ 正在解析磁力連結或查詢中繼資料\n通常幾秒內完成, 否則請嘗試切換資料來源或先快取再看
+ 正在緩衝
+ BT 初始緩衝耗時稍長, 請耐心等待 30 秒
+ 若持續沒有速度, 可嘗試切換資料來源
+ 緩衝耗時過長, 可嘗試切換資料來源
+ 載入失敗:
+ 解析逾時
+ 未知錯誤
+ 不支援該檔案類型
+ 未找到可播放的檔案
+ 已取消
+ 網路錯誤
+ 已想看,可更改為:
+ 選擇資料來源
+ 關閉選擇器
+ 相關推薦
+ %1$s 彈幕時間校準
+ 調整彈幕出現時間以匹配目前影片。正值表示彈幕延後,負值表示彈幕提前。
+ 目前偏移:%1$s
+ 重設為 0
+ 恢復原值
+ 暫無彈幕資料
+ 沒有符合篩選條件的彈幕
+ 彈幕列表
+ 收起
+ 展開
+ 更多選項
+ 外部連結
+ 停用
+ 啟用
+ 重新匹配
+ 時間校準 (%1$s)
+ 嗶
+ 彈
+ 巴
+ 正在播放
+ 彈幕設定
+ 快轉 85 秒
+ 摺疊側邊欄
+ 展開側邊欄
+ 預覽模式
+ 輸入要屏蔽的彈幕關鍵字(正則)
+ 正則表達式
+ 正則表達式語法不正確。
+ 例如:‘簽’ 會屏蔽含文字‘簽’的彈幕。
+ 添加
+ 刪除
+ 正則彈幕過濾管理
+ 頂部
+ 滾動
+ 底部
+ 彩色
+ 彈幕字號
+ 不透明度
+ 描邊寬度
+ 彈幕字重
+ 彈幕速度
+ 彈幕速度不會跟隨影片倍速變化
+ 同屏密度
+ 密集
+ 適中
+ 稀疏
+ 顯示區域
+ 關閉
+ 1/8 屏
+ 1/6 屏
+ 1/4 屏
+ 半屏
+ 3/4 屏
+ 全屏
+ 啟用正則彈幕過濾器
+ 管理正則彈幕過濾器
+ 彈幕除錯模式
diff --git a/app/shared/app-lang/src/androidMain/res/values/strings.xml b/app/shared/app-lang/src/androidMain/res/values/strings.xml
index feb8a98f6e..af5dbefecd 100644
--- a/app/shared/app-lang/src/androidMain/res/values/strings.xml
+++ b/app/shared/app-lang/src/androidMain/res/values/strings.xml
@@ -108,6 +108,43 @@
Step 1: Search for items
Step 2: Search for episodes
Step 3: Match videos
+ Name *
+ Set the name shown in the list
+ Icon URL
+ Search URL
+ Example: https://www.nyacg.net/search.html?wd={keyword}
+ Replacement rules:\n{keyword} is replaced with the item (anime) name
+ Base URL (optional)
+ Optional. Used to build the item details (episode list) page URL and affects Step 2. By default it is derived automatically from the search URL
+ Use only the first word
+ Split by spaces and use only the first word for search. Useful when the source has poor search compatibility
+ Remove special characters
+ Remove special characters and words like "movie" to improve search success rate
+ Number of item names to try
+ How many item names to query with for each playback.\n1 uses only the primary name, 2 also uses the original Japanese title, and values above 2 also use other aliases. The number of aliases is not fixed.\nUsually 1 is enough. Using multiple names will significantly increase wait time during playback.
+ Search request interval (ms)
+ Controls how long to wait after sending each request before sending the next one
+ Filter settings
+ Filter by item name
+ Require the resource title to contain the item name. Useful when the source may return irrelevant results. This only works in versions before 4.4.0 and has no effect in other versions
+ Filter by episode number
+ Require the resource title to contain the episode number. Useful when the source may return irrelevant results. Usually recommended
+ Mark resolution
+ Mark all resources from this source with this resolution. This does not affect queries; it is only used as a preference and filter when selecting resources in the player
+ Mark subtitle language
+ Mark all resources from this source with this subtitle language. This does not affect queries; it is only used as a preference and filter when selecting resources in the player
+ When selecting resources in the player
+ Distinguish item names
+ When off, all items found in Step 1 are treated as the same item, and episodes with the same title are deduplicated.\nWhen on, they are not deduplicated this way.\nThis does not affect test results. It affects results when selecting resources in the player.
+ Distinguish channel names
+ When off, episodes with the same title are deduplicated even if their channel names differ.\nWhen on, they are not deduplicated this way.\nThis does not affect test results. It affects results when selecting resources in the player.
+ When playing videos
+ Referer for HTTP requests made when playing videos. Can be left blank
+ User-Agent for HTTP requests made when playing videos
+ Single tag
+ Multiple tags
+ Do not distinguish channels
+ Group by channels
Enable nested links
When a matching link is encountered, stop loading the parent page and jump to the matching link, then continue searching for video links in the nested page. Supports unlimited nesting
Match nested links
@@ -183,6 +220,11 @@
Disabled, will open an external browser to complete download
Auto download updates
You will be prompted when the download finishes, and installation will only proceed upon confirmation
+ Installer package not found
+ Failed to open the file. Please install it manually: %1$s
+ View installer package
+ Cancel update
+ Automatic installation failed. Please install manually
Checking…
Check failed
Up to date
@@ -381,6 +423,19 @@
Search
Trending
Anime Schedule
+ View details
+ Ep. %1$s
+ Ep. %1$s (%2$s)
+ Mon
+ Tue
+ Wed
+ Thu
+ Fri
+ Sat
+ Sun
+ %1$s
+ Last %1$s
+ Next %1$s
Continue Watching
Recommendations
Hold Shift + mouse wheel to scroll horizontally
@@ -448,6 +503,10 @@
Custom
Auto-detection Result
Detecting
+ Checking connections. Please wait
+ Some services failed to connect. Consider enabling a proxy
+ Some services failed to connect. Try a different proxy mode or address
+ All services connected successfully
No system proxy detected
Proxy Address
Example: http://127.0.0.1:7890 or socks5://127.0.0.1:1080
@@ -455,6 +514,15 @@
Optional
None
Password
+ Retest
+ Danmaku service
+ Collection data service
+ Comment service
+ Connection successful
+ Connection failed
+ Proxy disabled
+ Using %1$s
+ Save and test
anitorrent
@@ -486,6 +554,239 @@
Logged out
Account
+
+ Welcome to Animeko
+ A one-stop online anime platform with danmaku (Ani for short)
+ Ani is maintained by the OpenAni community and contributors. It is completely free and open source on GitHub.
+ Ani aims to provide the simplest and most comfortable anime-watching experience possible.
+ Continue
+ Theme Settings
+ Network Settings
+ Welcome, %1$s
+ Welcome
+ Finish
+ Colors
+ Dynamic Colors
+ Use system accent colors
+ Ani supports streaming BT resources while downloading. BT speed depends on your network quality.
+ Allow notification permission to view download progress while caching.
+ Enable BitTorrent features
+ Allow notifications
+ Show BT download progress, speed, and other information
+ Authorized
+ Grant permission
+ Step %1$d / %2$d
+ Next
+ Previous
+ Skip
+ Choose Theme
+ Configure Proxy
+ BitTorrent Features
+
+
+ Sign In
+ Sign Up
+ Sign In / Sign Up
+ Bind Email
+ Change Email
+ Your Email Address
+ We will send you a verification email
+ Email
+ Clear
+ Continue
+ This email address is already in use
+ The verification code is invalid or expired. Please resend it.
+ Enter Verification Code
+ Please check your email: %1$s
+ Signing in to your existing account
+ Registering a new account
+ Verification Code
+ 6 digits
+ Resend verification code
+ Resend available in %1$d seconds
+ Other sign-in methods
+
+
+ Authorize Bangumi Sign-In
+ Authorize your Bangumi account to sync your watch history to Bangumi or sign in to Ani more conveniently.
+ Bind Bangumi Account
+ Sign In / Sign Up
+ Waiting for result
+ Authorized
+ Cancel
+ Help
+ What is Bangumi?
+ The browser says the website is blocked or inaccessible
+ Which option should I choose when registering?
+ The site keeps saying the captcha is wrong during registration or sign-in
+ I can\'t receive the email verification code
+ Account activation keeps failing during registration
+ Other issues
+ Bangumi is a Chinese ACGN community website for sharing and discussion.\nBangumi provides anime indexes, collections, and watch progress tracking. Ani can sync your watch history to Bangumi.
+ Change your default browser in system settings. Google Chrome, Microsoft Edge, and Mozilla Firefox are recommended.
+ Choose the option for managing ACG collections and watch progress, then sharing and discussing them.
+ If there is no captcha input box, try clicking the password box a few more times. If you entered the captcha incorrectly, refresh the page before signing in again.
+ Check your spam folder and try registering with a common email provider such as QQ Mail, NetEase Mail, or Outlook.
+ Delete the last character of the activation code, then type that character manually, or try another browser.
+ Can\'t solve your issue? You can also get help through the following channels.
+
+
+ Fit
+ Stretch
+ Crop
+ Auto
+ Audio
+ Off
+ Subtitle
+ Disable danmaku
+ Enable danmaku
+ Mute
+ Volume
+ Next episode
+ Episodes
+ Send
+ Speed
+ Skipping OP or ED soon
+ Cancel
+ Send a danmaku~
+ Careful, I\'m about to launch a danmaku!
+ Every danmaku hides a secret
+ Summon the danmaku spirit!
+ Only you understand this moment best
+ Make this danmaku different
+ The gate to the danmaku world is open
+ There are secrets hidden between the lines
+ In the weave of light and shadow, your words are the only truth
+ Interesting souls are one in a million
+ Say something
+ We\'re always collecting fun danmaku slogans
+ Ad space for rent
+ 🤔
+ Where dreams begin
+ Thoughts take shape
+ Send a danmaku and liven things up!
+ How about a danmaku?
+ Send a friendly danmaku!
+ Can\'t help wanting to send a danmaku?
+
+
+ Want to Watch
+ Watching
+ Watched
+ On Hold
+ Dropped
+ Unfollow
+ Follow
+ Marked Want to Watch
+ Marked Watching
+ Marked Watched
+ Marked On Hold
+ Marked Dropped
+ Not Followed
+ Unfollow
+ This will clear your watch progress and rating. This action cannot be undone. Are you sure you want to unfollow?
+ Delete
+ Cancel
+ Mark all episodes as watched too?
+ Set
+ Ignore
+ Syncing
+ Following
+ Move to "Watched"
+ Uncollected
+ In guest mode, please search and watch directly, or sign in to use the collection feature.
+ %1$s follows / %2$s watching
+ / %1$s dropped
+ %1$s/%2$s
+
+
+ Unmark Watched
+ Watched
+ Dropped
+ Mark Watched
+ Episode Selection
+ Long press to mark as watched
+ Item Details
+ Close
+ Cache
+ Collection State
+ Cache Type
+ Download Status
+ Downloading
+ Finished
+ Sort
+ Newest Download
+ Oldest Download
+ Title A-Z
+ Title Z-A
+ Episode Ascending
+ Episode Descending
+ Wish to Watch
+ Watching
+ Watched
+ On Hold
+ Dropped
+ Not Collected
+ Total Upload
+ Total Download
+ Cover
+ Resume Download
+ Pause Download
+ Manage Item
+ Download Completed
+ Download Failed
+ Downloading
+ Episode Cache
+ Delete
+ Cancel
+ Cache
+ Select Storage Location
+ Manage All Cache
+ Unknown
+ Select an item to view cache details
+ %1$d selected
+ Exit Selection
+ Select All
+ Delete Selected
+ Enter Selection Mode
+ Delete Cache
+ Deletion cannot be undone. Delete this cache?
+ %1$d/%2$d completed
+ %1$d downloading
+ Episode %1$d · %2$s
+ More Actions
+ Play
+ Cache information is invalid and cannot be played
+ This resource does not support streaming while downloading. Please wait for the download to finish
+ More Info
+ Details
+ Copied
+ Copy
+ Open Link
+ Failed to open file: %1$s
+ Browse File
+ Episode Range
+ Source
+ Online
+ Local
+ Subtitle Group
+ Subtitle Language
+ Publish Time
+ Resolution
+ File Size
+ Original Link
+ File Type
+ Original Download Link
+ Local Cache Path
+ Total Segments
+ Downloader Internal Status
+ External Subtitle %1$d
+ Not Signed In
+ Edit Profile
+ Sign In / Sign Up
+ Settings
+ Logout
+ Are you sure you want to sign out?
Bangumi login expired
Bangumi login expired. Please log in again.
@@ -501,6 +802,389 @@
Are you sure you want to logout?
Logout
Cancel
+ Manual Full Sync
+ Re-download All Bangumi Data
+ Download Bangumi collection data into Animeko\'s collection service. You usually don\'t need to do this, since Animeko syncs automatically. Only run it if you notice data inconsistencies. This operation may take several minutes, and other features will be unavailable during sync. Note: full sync can only be performed once every 10 minutes.
+ Sync Queue
+ Pending sync operations
+ Execute All
+ Loading...
+ Loading... Loading... Loading...
+ Update Collection: %1$d (%2$s)
+ Delete Collection: %1$d
+ Mark Episode Unwatched: %1$d
+ Mark Episode Watched: %1$d (%2$s)
+ Unknown operation (please update the app)
+ Nickname
+ Not Set
+ Up to 20 characters; Chinese, Japanese, English letters, numbers, and underscores only
+ Email
+ Bind
+ User ID
+ Third-Party Accounts
+ Not Bound
+ Unbind
+ Unbind Bangumi? Watch history will no longer sync to Bangumi. You can bind it again later.
+ Select Avatar
+ Done
+ Upload Avatar
+ Select File
+ Supports JPEG/PNG/WebP, up to 1MB. Multiple uploads require a one-minute interval.
+ Or drag a file here. Supports JPEG/PNG/WebP, up to 1MB. Multiple uploads require a one-minute interval.
+ Uploading...
+ Image size exceeds 1MB
+ Unsupported image format
+ Crop and Upload
+ Crop Avatar
+ Drag the selection to move it, or drag the corners to resize
+ Follow an anime as "%1$s" and it will appear here
+ Sign in to follow
+ Coming soon
+ Write Review
+ Details
+ Reviews
+ Discussions
+ Characters
+ Characters %1$d
+ Staff
+ Staff %1$d
+ Related titles
+ Show less
+ Show more
+ View all
+ Prequel
+ Sequel
+ Spin-off
+ Special
+ Network error
+ You\'re doing that too fast. Please try again
+ Service unavailable
+ No results
+ This feature requires sign-in
+ Unknown error: %1$s
+ No details
+ Request error: %1$s
+ Copied. Please report it on GitHub issues or in the group chat
+ An unknown error occurred. Please report it in Settings with logs attached
+ Operation failed, please try again
+ Operation failed
+ Embedded
+ Closed
+ External
+ Unknown
+ Closed or Unknown
+ Cantonese
+ Simplified Chinese
+ Traditional Chinese
+ Japanese
+ English
+ Resolution
+ Subtitle
+ Fansub
+ Expand
+ Clear filter
+ Selected
+ No subtitles
+ Single-episode resource
+ Unsupported playback
+ Season mismatch
+ Title mismatch
+ Invalid request. Please check it
+ Save and refresh
+ Edit request
+ Primary search name
+ Most data sources only use this name
+ Secondary search names
+ Online sources ignore these names
+ Collapse
+ Expand
+ Delete name %1$d
+ Add name
+ Episode info
+ A resource must match at least one of the following two pieces of information, otherwise it will not be shown. You can change only one of them
+ Series episode number
+ Assuming there are two seasons with 12 episodes each, the first episode of season 2 would be 13
+ Season episode number
+ The number within the current season, for example, the first episode of season 2 is 01
+ Discard
+ Continue editing
+ You have unsaved changes. Discard them?
+ Not right?
+ Edit query
+ Waiting for captcha handling
+ Manual select
+ Change
+ Automatically selecting a data source
+ Please select a data source
+ Data source
+ Searched:
+ Searching
+ Searched
+ %1$s %2$d/%3$d data sources
+ Help
+ Settings
+ Tap to enable temporarily
+ Query failed
+ Tap to retry
+ Captcha required
+ Tap to verify
+ Query succeeded
+ Verify
+ Data source help
+ Data source types
+ Fetch resources from the BitTorrent network. Quality is usually higher and coverage is broader, but loading may be slower
+ Fetch resources from online video sites. Loading is usually faster, but quality is often lower
+ Simple mode
+ Detailed mode
+ Filtered %1$d/%2$d resources
+ Show excluded resources (%1$d)
+ Unable to open the link
+ Open original link %1$s
+ Edit configuration
+ Based on the Step 3 configuration, no playable link was matched from %1$d links. Please check the configuration
+ Based on the Step 3 configuration, %2$d playable link was matched from %1$d links
+ Based on the Step 3 configuration, %2$d playable links were matched from %1$d links. For better stability, adjust the rules so exactly one link is matched
+ Hide images
+ Hide CSS/fonts
+ Hide JS/WASM
+ Hide data
+ Actual playback: %1$s
+ Nested link
+ Matched
+ Not matched
+ Episode %1$s
+ Cached
+ Danmaku count
+ Disabled
+ Danmaku settings for %1$s
+ Bilibili
+ Dandanplay
+ Exact match
+ Semi-fuzzy match
+ Fuzzy match
+ No match
+ stream link
+ torrent download link
+ local file link
+ magnet link
+ webpage link
+ Copy %1$s
+ Open %1$s
+ Open with another app
+ Copy source page link
+ Open source page
+ Episode list
+ View more episodes
+ Continue watching %1$s
+ Finished
+ Not yet aired
+ Starts %1$s
+ Start watching
+ Updates %1$s
+ Watched %1$s
+ Unknown
+ Not yet aired
+ On air
+ Up to %1$s
+ Completed
+ All %1$s eps
+ Planned %1$s eps
+ Comments
+ Comments %1$d
+ Share
+ Download
+ Change danmaku
+ Loading episode list…
+ Loading danmaku list…
+ Select subject
+ Select episode
+ Send
+ Back to top
+ %1$d results shown
+ Best match
+ Most follows
+ Highest rank
+ Release date
+ Staff:
+ Audience
+ Category
+ Character
+ Emotion
+ Genre
+ Rating
+ Region
+ Series
+ Setting
+ Source
+ Technology
+ Custom
+ Background running
+ Caching downloads require the app to keep running in the background
+ Ignore battery optimizations
+ Helps keep the app running in the background. May increase battery use
+ Notification settings
+ Open settings
+ Danmaku refresh rate
+ Share today\'s log file
+ Share log file
+ Copy today\'s log content (large)
+ Log file not found
+ Follow this anime before rating
+ Discard changes
+ Your rating has not been saved. Discard it?
+ Discard
+ Edit rating
+ Comment
+ Say something...
+ Optional
+ Visible only to you
+ Terrible (rate carefully)
+ Very bad
+ Bad
+ Poor
+ Average
+ Okay
+ Recommended
+ Highly recommended
+ Masterpiece
+ Legendary (rate carefully)
+ Your rating: %1$d
+ %1$d ratings | #%2$d
+ Failed to send: network error
+ Failed to send. Please report this issue with logs attached.\n%1$s
+ Comments are posted to Ani. Bangumi comments are read-only
+ Write a comment
+ Rendering...
+ Reply to comment
+ View %1$d more replies >
+ Add emoji
+ Bold
+ Italic
+ Underline
+ Strikethrough
+ Spoiler
+ Image
+ Link
+ More comment editor actions
+ Edit
+ Preview
+ Send
+ Unable to open the link. It has been copied to the clipboard. Please open it in your browser
+ This link may open another app. ani will not open this link:\n
+ Unable to open this link:\n
+ This content cannot be shown
+ Show temporarily
+ Downloading Bangumi collection data
+ This may take 5-15 minutes. Please be patient. You can keep using other features during the download. Refresh the collection list manually to see the latest progress.
+ Continue in background
+ Preparing
+ Fetching metadata
+ (Done %1$d items) Fetching more collection list
+ (Done %1$d items) Fetching watch progress
+ (Done %1$d items) Saving
+ (Done %1$d items) Finishing
+ (Done %1$d items) Sync failed. Error details:\n%2$s
+ (Done %1$d items) Sync completed
+ In progress
+ No more items
+ Previous group
+ Select group
+ Next group
+ #%1$d\n%2$d ratings
+ Copy
+ Error details
+ Error
+ No matching file
+ Resolution timed out
+ Unsupported video type
+ Unknown error, tap to view
+ Cancelled
+ Network error. Please check your connection
+ Failed to load danmaku. Tap to view
+ %1$d danmaku sources, %2$d danmaku in total
+ Show less
+ Show more
+ Danmaku is disabled. You can enable it in the player
+ Loading danmaku
+ Now playing:
+ Error message
+ Playback failed. Please switch resources
+ Automatically selecting a resource. Please wait
+ Resolving resource link\nThis usually finishes in a few seconds. Otherwise, try switching resources
+ Resource resolved. Preparing video
+ Resolving magnet link or querying metadata\nThis usually finishes in a few seconds. Otherwise, try switching resources or cache first
+ Buffering
+ Initial BT buffering may take longer. Please wait 30 seconds
+ If speed stays at zero, try switching resources
+ Buffering is taking too long. Try switching resources
+ Load failed:
+ Resolution timed out
+ Unknown error
+ Unsupported file type
+ No playable file found
+ Cancelled
+ Network error
+ Already marked as want to watch. Change to:
+ Select resource
+ Close selector
+ Related recommendations
+ %1$s danmaku timing adjustment
+ Adjust danmaku timing to match the current video. Positive values delay danmaku, negative values advance it.
+ Current offset: %1$s
+ Reset to 0
+ Restore original
+ No danmaku data
+ No danmaku matches the current filters
+ Danmaku list
+ Collapse
+ Expand
+ More options
+ External links
+ Disable
+ Enable
+ Rematch
+ Time shift (%1$s)
+ B
+ D
+ BA
+ Now playing
+ Danmaku settings
+ Fast forward 85 seconds
+ Collapse sidebar
+ Expand sidebar
+ Preview mode
+ Enter a regex keyword to filter danmaku
+ Regular expression
+ The regular expression is invalid.
+ Example: \'sign\' filters danmaku containing \'sign\'.
+ Add
+ Delete
+ Manage regex danmaku filters
+ Top
+ Scrolling
+ Bottom
+ Color
+ Danmaku size
+ Opacity
+ Outline width
+ Font weight
+ Danmaku speed
+ Danmaku speed does not follow video playback speed
+ Density
+ Dense
+ Medium
+ Sparse
+ Display area
+ Off
+ 1/8 screen
+ 1/6 screen
+ 1/4 screen
+ Half screen
+ 3/4 screen
+ Full screen
+ Enable regex danmaku filters
+ Manage regex danmaku filters
+ Danmaku debug mode
diff --git a/app/shared/src/commonMain/kotlin/ui/danmaku/DanmakuEditor.kt b/app/shared/src/commonMain/kotlin/ui/danmaku/DanmakuEditor.kt
index b20ec7b46f..3e3a744542 100644
--- a/app/shared/src/commonMain/kotlin/ui/danmaku/DanmakuEditor.kt
+++ b/app/shared/src/commonMain/kotlin/ui/danmaku/DanmakuEditor.kt
@@ -42,11 +42,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import me.him188.ani.app.data.models.preference.VideoScaffoldConfig
import me.him188.ani.app.ui.foundation.text.ProvideContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.episode_send_danmaku
import me.him188.ani.app.videoplayer.ui.PlayerControllerState
import me.him188.ani.app.videoplayer.ui.progress.PlayerControllerDefaults
import me.him188.ani.app.videoplayer.ui.rememberAlwaysOnRequester
import me.him188.ani.danmaku.api.DanmakuContent
import me.him188.ani.danmaku.api.DanmakuLocation
+import org.jetbrains.compose.resources.stringResource
import org.openani.mediamp.MediampPlayer
import org.openani.mediamp.isPlaying
@@ -172,6 +175,7 @@ fun DummyDanmakuEditor(
modifier: Modifier = Modifier,
) {
val shape = MaterialTheme.shapes.medium
+ val sendDanmakuText = stringResource(Lang.episode_send_danmaku)
Row(
modifier,
horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -191,7 +195,7 @@ fun DummyDanmakuEditor(
) {
ProvideContentColor(MaterialTheme.colorScheme.onSurfaceVariant) {
Text(
- "发送弹幕",
+ sendDanmakuText,
style = MaterialTheme.typography.labelLarge,
)
diff --git a/app/shared/src/commonMain/kotlin/ui/main/MainScreen.kt b/app/shared/src/commonMain/kotlin/ui/main/MainScreen.kt
index 5fb3ff385a..f3b9daf8f5 100644
--- a/app/shared/src/commonMain/kotlin/ui/main/MainScreen.kt
+++ b/app/shared/src/commonMain/kotlin/ui/main/MainScreen.kt
@@ -87,6 +87,7 @@ import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.foundation.widgets.showLoadError
import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.exploration_search
import me.him188.ani.app.ui.lang.settings
import me.him188.ani.app.ui.lang.settings_update_version_expired_copied_to_clipboard
import me.him188.ani.app.ui.lang.settings_update_version_expired_export_settings
@@ -182,7 +183,7 @@ private fun MainScreenContent(
},
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
) {
- Icon(Icons.Rounded.Search, "搜索")
+ Icon(Icons.Rounded.Search, stringResource(Lang.exploration_search))
}
},
navigationRailFooter = {
diff --git a/app/shared/src/commonMain/kotlin/ui/profile/UserInfoRow.kt b/app/shared/src/commonMain/kotlin/ui/profile/UserInfoRow.kt
index 909d8d459c..2ee86e2b7f 100644
--- a/app/shared/src/commonMain/kotlin/ui/profile/UserInfoRow.kt
+++ b/app/shared/src/commonMain/kotlin/ui/profile/UserInfoRow.kt
@@ -38,6 +38,9 @@ import me.him188.ani.app.data.models.UserInfo
import me.him188.ani.app.ui.external.placeholder.placeholder
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.avatar.AvatarImage
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings
+import org.jetbrains.compose.resources.stringResource
@Composable
@@ -119,7 +122,7 @@ fun UserInfoRow(
Column(Modifier.align(Alignment.Top)) {
IconButton(onClickSettings) {
- Icon(Icons.Rounded.Settings, "设置")
+ Icon(Icons.Rounded.Settings, stringResource(Lang.settings))
}
}
}
diff --git a/app/shared/src/commonMain/kotlin/ui/profile/auth/AniContactList.kt b/app/shared/src/commonMain/kotlin/ui/profile/auth/AniContactList.kt
index 057114ab7c..8b3b55b72c 100644
--- a/app/shared/src/commonMain/kotlin/ui/profile/auth/AniContactList.kt
+++ b/app/shared/src/commonMain/kotlin/ui/profile/auth/AniContactList.kt
@@ -28,7 +28,11 @@ import me.him188.ani.app.ui.foundation.icons.AniIcons
import me.him188.ani.app.ui.foundation.icons.GithubMark
import me.him188.ani.app.ui.foundation.icons.QqRoundedOutline
import me.him188.ani.app.ui.foundation.icons.Telegram
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_about_qq_group
+import me.him188.ani.app.ui.lang.settings_about_website
import me.him188.ani.app.ui.settings.tabs.AniHelperDestination
+import org.jetbrains.compose.resources.stringResource
private val ContactIconSize = 24.dp
@@ -38,6 +42,8 @@ fun AniContactList(
) {
val browserNavigator = rememberAsyncBrowserNavigator()
val context = LocalContext.current
+ val websiteText = stringResource(Lang.settings_about_website)
+ val qqGroupText = stringResource(Lang.settings_about_qq_group)
FlowRow(
modifier,
@@ -55,22 +61,24 @@ fun AniContactList(
{ browserNavigator.openBrowser(context, AniHelperDestination.ANI_WEBSITE) },
icon = {
Icon(
- Icons.Rounded.Public, "官网",
+ Icons.Rounded.Public,
+ websiteText,
Modifier.size(ContactIconSize),
)
},
- label = { Text("官网") },
+ label = { Text(websiteText) },
)
SuggestionChip(
{ browserNavigator.openJoinGroup(context) },
icon = {
Icon(
- AniIcons.QqRoundedOutline, "QQ 群",
+ AniIcons.QqRoundedOutline,
+ qqGroupText,
Modifier.size(ContactIconSize),
)
},
- label = { Text("QQ 群") },
+ label = { Text(qqGroupText) },
)
SuggestionChip(
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeCollectionActionButton.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeCollectionActionButton.kt
index 9bcb747e03..74e12ff274 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeCollectionActionButton.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeCollectionActionButton.kt
@@ -33,15 +33,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.external.placeholder.placeholder
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.subject.collection.components.EditCollectionTypeDropDown
import me.him188.ani.app.ui.subject.collection.components.SubjectCollectionAction
import me.him188.ani.app.ui.subject.collection.components.SubjectCollectionActions
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
+import org.jetbrains.compose.resources.*
private val ACTIONS = listOf(
SubjectCollectionAction(
- { Text("取消看过") },
+ { Text(stringResource(Lang.subject_episode_unwatch)) },
{ Icon(Icons.Rounded.AccessTime, null) },
UnifiedCollectionType.WISH,
),
@@ -89,18 +91,18 @@ fun EpisodeCollectionActionButton(
) {
when (collectionType) {
UnifiedCollectionType.DONE -> {
- Text("已看过")
+ Text(stringResource(Lang.subject_episode_watched))
}
UnifiedCollectionType.DROPPED -> {
- Text("已抛弃")
+ Text(stringResource(Lang.subject_episode_dropped))
}
else -> {
Box(Modifier.size(16.dp)) {
Icon(Icons.Rounded.Add, null)
}
- Text("看过", Modifier.padding(start = 8.dp))
+ Text(stringResource(Lang.subject_episode_mark_watched), Modifier.padding(start = 8.dp))
}
}
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodePage.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodePage.kt
index 1aa00a4978..829d59e542 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodePage.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodePage.kt
@@ -127,6 +127,13 @@ import me.him188.ani.app.ui.foundation.theme.LocalThemeSettings
import me.him188.ani.app.ui.foundation.theme.weaken
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.foundation.widgets.showLoadError
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.episode_comments
+import me.him188.ani.app.ui.lang.episode_comments_with_count
+import me.him188.ani.app.ui.lang.episode_send_danmaku
+import me.him188.ani.app.ui.lang.foundation_richtext_external_app_link_warning_prefix
+import me.him188.ani.app.ui.lang.foundation_richtext_open_failed_prefix
+import me.him188.ani.app.ui.lang.subject_details_tab_details
import me.him188.ani.app.ui.richtext.RichTextDefaults
import me.him188.ani.app.ui.subject.episode.comments.EpisodeCommentColumn
import me.him188.ani.app.ui.subject.episode.comments.EpisodeEditCommentSheet
@@ -147,7 +154,7 @@ import me.him188.ani.app.videoplayer.ui.gesture.LevelController
import me.him188.ani.app.videoplayer.ui.gesture.NoOpLevelController
import me.him188.ani.app.videoplayer.ui.gesture.asLevelController
import me.him188.ani.app.videoplayer.ui.progress.PlayerControllerDefaults
-import me.him188.ani.app.videoplayer.ui.progress.PlayerControllerDefaults.randomDanmakuPlaceholder
+import me.him188.ani.app.videoplayer.ui.progress.PlayerControllerDefaults.rememberRandomDanmakuPlaceholder
import me.him188.ani.app.videoplayer.ui.progress.rememberMediaProgressSliderState
import me.him188.ani.danmaku.api.DanmakuContent
import me.him188.ani.danmaku.api.DanmakuLocation
@@ -158,6 +165,7 @@ import me.him188.ani.utils.platform.isAndroid
import me.him188.ani.utils.platform.isDesktop
import me.him188.ani.utils.platform.isIos
import me.him188.ani.utils.platform.isMobile
+import org.jetbrains.compose.resources.stringResource
import org.openani.mediamp.features.AudioLevelController
import org.openani.mediamp.features.PlaybackSpeed
import org.openani.mediamp.features.Screenshots
@@ -573,6 +581,7 @@ private fun TabRow(
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.surface,
) {
+ val detailsText = stringResource(Lang.subject_details_tab_details)
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
modifier,
@@ -590,7 +599,7 @@ private fun TabRow(
selected = pagerState.currentPage == 0,
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
modifier = Modifier.height(44.dp),
- text = { Text("详情", softWrap = false) },
+ text = { Text(detailsText, softWrap = false) },
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
)
@@ -599,11 +608,11 @@ private fun TabRow(
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
modifier = Modifier.height(44.dp),
text = {
- val text by remember(commentCount) {
- derivedStateOf {
- val count = commentCount()
- if (count == null) "评论" else "评论 $count"
- }
+ val count = commentCount()
+ val text = if (count == null) {
+ stringResource(Lang.episode_comments)
+ } else {
+ stringResource(Lang.episode_comments_with_count, count)
}
Text(text, softWrap = false)
},
@@ -779,13 +788,13 @@ private fun DetachedDanmakuEditorLayout(
modifier: Modifier = Modifier,
) {
Column(modifier.padding(all = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
- Text("发送弹幕", style = MaterialTheme.typography.titleMedium)
+ Text(stringResource(Lang.episode_send_danmaku), style = MaterialTheme.typography.titleMedium)
val isSending = danmakuEditorState.isSending.collectAsStateWithLifecycle()
PlayerDanmakuEditor(
text = danmakuEditorState.text,
onTextChange = { danmakuEditorState.text = it },
isSending = { isSending.value },
- placeholderText = remember { randomDanmakuPlaceholder() },
+ placeholderText = rememberRandomDanmakuPlaceholder(),
onSend = onSend,
Modifier.fillMaxWidth().focusRequester(focusRequester),
colors = OutlinedTextFieldDefaults.colors(),
@@ -873,7 +882,7 @@ private fun EpisodeVideo(
}
// Refresh every time on configuration change (i.e. switching theme, entering fullscreen)
- val danmakuTextPlaceholder = remember { randomDanmakuPlaceholder() }
+ val danmakuTextPlaceholder = rememberRandomDanmakuPlaceholder()
val window = LocalPlatformWindow.current
SideEffect {
@@ -1084,6 +1093,8 @@ private fun EpisodeCommentColumn(
) {
val toaster = LocalToaster.current
val browserNavigator = LocalUriHandler.current
+ val externalAppLinkWarningPrefix = stringResource(Lang.foundation_richtext_external_app_link_warning_prefix)
+ val openLinkFailedPrefix = stringResource(Lang.foundation_richtext_open_failed_prefix)
EpisodeCommentColumn(
state = commentState,
@@ -1100,7 +1111,13 @@ private fun EpisodeCommentColumn(
setShowEditCommentSheet(true)
},
onClickUrl = {
- RichTextDefaults.checkSanityAndOpen(it, browserNavigator, toaster)
+ RichTextDefaults.checkSanityAndOpen(
+ it,
+ browserNavigator,
+ toaster,
+ externalAppLinkWarningPrefix,
+ openLinkFailedPrefix,
+ )
},
modifier = modifier.fillMaxSize(),
gridState = gridState,
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeVideo.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeVideo.kt
index 654d8b9482..b51dfd6be4 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeVideo.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeVideo.kt
@@ -71,6 +71,16 @@ import me.him188.ani.app.ui.foundation.ifThen
import me.him188.ani.app.ui.foundation.interaction.WindowDragArea
import me.him188.ani.app.ui.foundation.rememberDebugSettingsViewModel
import me.him188.ani.app.ui.foundation.theme.AniTheme
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_cache
+import me.him188.ani.app.ui.lang.subject_episode_collapse_sidebar
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_settings_title
+import me.him188.ani.app.ui.lang.subject_episode_expand_sidebar
+import me.him188.ani.app.ui.lang.subject_episode_external_links
+import me.him188.ani.app.ui.lang.subject_episode_fast_forward_85_seconds
+import me.him188.ani.app.ui.lang.subject_episode_more_options
+import me.him188.ani.app.ui.lang.subject_episode_preview_mode
+import me.him188.ani.app.ui.lang.subject_episode_select_media_source
import me.him188.ani.app.ui.mediafetch.TestMediaSourceResultListPresentation
import me.him188.ani.app.ui.mediafetch.ViewKind
import me.him188.ani.app.ui.mediafetch.rememberTestMediaSelectorState
@@ -124,6 +134,7 @@ import me.him188.ani.app.videoplayer.ui.top.SystemTime
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.isDesktop
import me.him188.ani.utils.platform.isMobile
+import org.jetbrains.compose.resources.stringResource
import org.openani.mediamp.MediampPlayer
import org.openani.mediamp.features.audioTracks
import org.openani.mediamp.features.subtitleTracks
@@ -187,6 +198,7 @@ internal fun EpisodeVideoImpl(
var isLocked by remember { mutableStateOf(false) }
val sheetsController = rememberVideoSideSheetsController()
val anySideSheetVisible by sheetsController.hasPageAsState()
+ val previewModeText = stringResource(Lang.subject_episode_preview_mode)
// auto hide cursor
val videoInteractionSource = remember { MutableInteractionSource() }
@@ -244,7 +256,7 @@ internal fun EpisodeVideoImpl(
},
video = {
if (LocalIsPreviewing.current) {
- Text("预览模式")
+ Text(previewModeText)
} else {
// Save the status bar height to offset the video player
val statusBarHeight by rememberStatusBarHeightAsState()
@@ -475,6 +487,14 @@ private fun EpisodeVideoTopBarActions(
var showMoreDropdown by rememberSaveable { mutableStateOf(false) }
val dropdownAlwaysOnRequester = rememberAlwaysOnRequester(playerControllerState, "topBarExternalActions")
val isExternalDropdownVisible = showShareDropdown || showMoreDropdown
+ val fastForward85SecondsText = stringResource(Lang.subject_episode_fast_forward_85_seconds)
+ val selectMediaSourceText = stringResource(Lang.subject_episode_select_media_source)
+ val danmakuSettingsTitleText = stringResource(Lang.subject_episode_danmaku_settings_title)
+ val moreOptionsText = stringResource(Lang.subject_episode_more_options)
+ val externalLinksText = stringResource(Lang.subject_episode_external_links)
+ val cacheText = stringResource(Lang.subject_episode_cache)
+ val collapseSidebarText = stringResource(Lang.subject_episode_collapse_sidebar)
+ val expandSidebarText = stringResource(Lang.subject_episode_expand_sidebar)
DisposableEffect(dropdownAlwaysOnRequester, isExternalDropdownVisible) {
if (isExternalDropdownVisible) {
@@ -490,7 +510,7 @@ private fun EpisodeVideoTopBarActions(
}
IconButton({ onClickSkip85(playerState.getCurrentPositionMillis()) }) {
- Icon(AniIcons.Forward85, "快进 85 秒")
+ Icon(AniIcons.Forward85, fastForward85SecondsText)
}
if (expanded) {
@@ -498,7 +518,7 @@ private fun EpisodeVideoTopBarActions(
{ sheetsController.navigateTo(EpisodeVideoSideSheetPage.MEDIA_SELECTOR) },
Modifier.testTag(TAG_SHOW_MEDIA_SELECTOR),
) {
- Icon(Icons.Rounded.DisplaySettings, contentDescription = "数据源")
+ Icon(Icons.Rounded.DisplaySettings, contentDescription = selectMediaSourceText)
}
}
@@ -506,19 +526,19 @@ private fun EpisodeVideoTopBarActions(
{ sheetsController.navigateTo(EpisodeVideoSideSheetPage.PLAYER_SETTINGS) },
Modifier.testTag(TAG_SHOW_SETTINGS),
) {
- Icon(AniIcons.SubtitleGear, contentDescription = "弹幕设置")
+ Icon(AniIcons.SubtitleGear, contentDescription = danmakuSettingsTitleText)
}
Box {
IconButton({ showMoreDropdown = true }) {
- Icon(Icons.Rounded.MoreVert, contentDescription = "更多")
+ Icon(Icons.Rounded.MoreVert, contentDescription = moreOptionsText)
}
DropdownMenu(
expanded = showMoreDropdown,
onDismissRequest = { showMoreDropdown = false },
) {
DropdownMenuItem(
- text = { Text("外部链接") },
+ text = { Text(externalLinksText) },
onClick = {
showMoreDropdown = false
showShareDropdown = true
@@ -526,7 +546,7 @@ private fun EpisodeVideoTopBarActions(
leadingIcon = { Icon(Icons.AutoMirrored.Rounded.OpenInNew, null) },
)
DropdownMenuItem(
- text = { Text("离线缓存") },
+ text = { Text(cacheText) },
onClick = {
showMoreDropdown = false
onClickCache()
@@ -547,9 +567,9 @@ private fun EpisodeVideoTopBarActions(
Modifier.testTag(TAG_COLLAPSE_SIDEBAR),
) {
if (sidebarVisible) {
- Icon(AniIcons.RightPanelClose, contentDescription = "折叠侧边栏")
+ Icon(AniIcons.RightPanelClose, contentDescription = collapseSidebarText)
} else {
- Icon(AniIcons.RightPanelOpen, contentDescription = "展开侧边栏")
+ Icon(AniIcons.RightPanelOpen, contentDescription = expandSidebarText)
}
}
}
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/comments/EpisodeCommentColumn.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/comments/EpisodeCommentColumn.kt
index 57484a0202..39517d28f0 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/comments/EpisodeCommentColumn.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/comments/EpisodeCommentColumn.kt
@@ -56,15 +56,18 @@ import me.him188.ani.app.ui.comment.UICommentSource
import me.him188.ani.app.ui.comment.UIRichText
import me.him188.ani.app.ui.comment.generateUiComment
import me.him188.ani.app.ui.comment.rememberTestCommentState
+import me.him188.ani.app.domain.foundation.LoadError
import me.him188.ani.app.ui.foundation.LocalImageViewerHandler
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.foundation.widgets.showLoadError
-import me.him188.ani.app.domain.foundation.LoadError
import me.him188.ani.app.ui.foundation.layout.plus
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.comment_send_comment
import me.him188.ani.app.ui.richtext.RichText
import me.him188.ani.app.ui.richtext.UIRichElement
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
@Composable
fun EpisodeCommentColumn(
@@ -76,6 +79,7 @@ fun EpisodeCommentColumn(
gridState: LazyGridState = rememberLazyGridState(),
) {
val imageViewer = LocalImageViewerHandler.current
+ val writeCommentText = stringResource(Lang.comment_send_comment)
val toaster = LocalToaster.current
LaunchedEffect(state) {
state.reactionSubmitFailures.collect { error ->
@@ -87,11 +91,10 @@ fun EpisodeCommentColumn(
modifier,
floatingActionButton = {
ExtendedFloatingActionButton(
- text = { Text("写评论") },
+ text = { Text(writeCommentText) },
icon = {
Icon(Icons.Rounded.AddComment, null)
- }
- ,
+ },
onClick = onNewCommentClick,
expanded = !gridState.canScrollBackward,
)
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/DanmakuListSection.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/DanmakuListSection.kt
index 0074b12994..a2e7dc2ef0 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/DanmakuListSection.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/DanmakuListSection.kt
@@ -74,10 +74,25 @@ import androidx.compose.ui.unit.sp
import me.him188.ani.app.ui.foundation.Res
import me.him188.ani.app.ui.foundation.a
import me.him188.ani.app.ui.foundation.lists.LazyListVerticalScrollbar
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_collapse
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_list_empty
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_list_empty_filtered
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_list_title
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_rematch
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_service_baha_short
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_service_bilibili_short
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_service_dandanplay_short
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_time_shift_item
+import me.him188.ani.app.ui.lang.subject_episode_disable
+import me.him188.ani.app.ui.lang.subject_episode_enable
+import me.him188.ani.app.ui.lang.subject_episode_expand
+import me.him188.ani.app.ui.lang.subject_episode_more_options
import me.him188.ani.app.ui.subject.episode.details.components.formatDanmakuShiftMillis
import me.him188.ani.app.ui.subject.episode.details.components.renderDanmakuServiceId
import me.him188.ani.danmaku.api.DanmakuServiceId
import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.stringResource
/**
* 弹幕列表区域组件,提供弹幕源选择和弹幕列表显示功能。
@@ -93,6 +108,9 @@ fun DanmakuListSection(
onAdjustShift: (DanmakuServiceId) -> Unit,
modifier: Modifier = Modifier,
) {
+ val listTitleText = stringResource(Lang.subject_episode_danmaku_list_title)
+ val collapseText = stringResource(Lang.subject_episode_collapse)
+ val expandText = stringResource(Lang.subject_episode_expand)
Box(modifier = modifier.padding(horizontal = 16.dp).fillMaxWidth()) {
Column {
@@ -127,7 +145,7 @@ fun DanmakuListSection(
) {
ListItem(
headlineContent = {
- Text("弹幕列表")
+ Text(listTitleText)
},
leadingContent = {
Icon(Icons.AutoMirrored.Outlined.FeaturedPlayList, contentDescription = null)
@@ -135,7 +153,7 @@ fun DanmakuListSection(
trailingContent = {
Icon(
if (expanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore,
- contentDescription = if (expanded) "收起" else "展开",
+ contentDescription = if (expanded) collapseText else expandText,
)
},
modifier = Modifier.clickable { onToggleExpanded() },
@@ -159,6 +177,12 @@ fun DanmakuListContent(
onAdjustShift: (DanmakuServiceId) -> Unit,
modifier: Modifier = Modifier,
) {
+ val emptyText = if (state.isEmpty) {
+ stringResource(Lang.subject_episode_danmaku_list_empty)
+ } else {
+ stringResource(Lang.subject_episode_danmaku_list_empty_filtered)
+ }
+
Column(modifier = modifier) {
// 弹幕源chips
if (state.sourceItems.isNotEmpty()) {
@@ -183,7 +207,7 @@ fun DanmakuListContent(
CircularProgressIndicator()
} else {
Text(
- text = if (state.isEmpty) "暂无弹幕数据" else "没有符合筛选条件的弹幕",
+ text = emptyText,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -261,6 +285,14 @@ private fun DanmakuSourceChip(
) {
var showDropdown by rememberSaveable { mutableStateOf(false) }
val isAnimeko = sourceItem.serviceId == DanmakuServiceId.Animeko
+ val moreOptionsText = stringResource(Lang.subject_episode_more_options)
+ val disableText = stringResource(Lang.subject_episode_disable)
+ val enableText = stringResource(Lang.subject_episode_enable)
+ val rematchText = stringResource(Lang.subject_episode_danmaku_rematch)
+ val timeShiftText = stringResource(
+ Lang.subject_episode_danmaku_time_shift_item,
+ formatDanmakuShiftMillis(sourceItem.shiftMillis),
+ )
Box {
FilterChip(
@@ -275,7 +307,7 @@ private fun DanmakuSourceChip(
Icon(
Icons.Outlined.ArrowDropDown,
- contentDescription = "更多选项",
+ contentDescription = moreOptionsText,
modifier = Modifier
.offset(x = 8.dp)
.clickable { showDropdown = true },
@@ -328,7 +360,7 @@ private fun DanmakuSourceChip(
// 操作菜单项
DropdownMenuItem(
- text = { Text(if (sourceItem.enabled) "禁用" else "启用") },
+ text = { Text(if (sourceItem.enabled) disableText else enableText) },
leadingIcon = {
Icon(
if (sourceItem.enabled) Icons.Outlined.Close else Icons.Outlined.CheckCircle,
@@ -342,7 +374,7 @@ private fun DanmakuSourceChip(
)
if (!isAnimeko) {
DropdownMenuItem(
- text = { Text("重新匹配") },
+ text = { Text(rematchText) },
leadingIcon = { Icon(Icons.Outlined.Refresh, null) },
onClick = {
onManualMatch()
@@ -351,7 +383,7 @@ private fun DanmakuSourceChip(
)
}
DropdownMenuItem(
- text = { Text("时间校准 (${formatDanmakuShiftMillis(sourceItem.shiftMillis)})") },
+ text = { Text(timeShiftText) },
leadingIcon = { Icon(Icons.Outlined.Schedule, null) },
onClick = {
onAdjustShift()
@@ -409,10 +441,10 @@ private fun DanmakuServiceIcon(
@Composable
private fun getDanmakuServiceIconInfo(serviceId: DanmakuServiceId): String {
return when (serviceId) {
- DanmakuServiceId.Bilibili -> "哔"
- DanmakuServiceId.Dandanplay -> "弹"
+ DanmakuServiceId.Bilibili -> stringResource(Lang.subject_episode_danmaku_service_bilibili_short)
+ DanmakuServiceId.Dandanplay -> stringResource(Lang.subject_episode_danmaku_service_dandanplay_short)
DanmakuServiceId.AcFun -> "Ac"
- DanmakuServiceId.Baha -> "巴"
+ DanmakuServiceId.Baha -> stringResource(Lang.subject_episode_danmaku_service_baha_short)
DanmakuServiceId.Tucao -> "TC"
else -> "?"
}
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeCarousel.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeCarousel.kt
index a7342fac50..369f427dde 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeCarousel.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeCarousel.kt
@@ -50,6 +50,10 @@ import me.him188.ani.app.tools.toProgress
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.icons.PlayingIcon
import me.him188.ani.app.ui.foundation.lists.PaginatedGroup
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.episode_comments
+import me.him188.ani.app.ui.lang.subject_episode_cached
+import me.him188.ani.app.ui.lang.subject_episode_default_title
import me.him188.ani.app.ui.subject.episode.details.components.EpisodeWatchStatusButton
import me.him188.ani.app.ui.subject.episode.details.components.PlayingEpisodeItem
import me.him188.ani.datasources.api.topic.FileSize.Companion.megaBytes
@@ -57,6 +61,7 @@ import me.him188.ani.datasources.api.topic.UnifiedCollectionType
import me.him188.ani.datasources.api.topic.isDoneOrDropped
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.format1f
+import org.jetbrains.compose.resources.stringResource
/**
@@ -124,6 +129,7 @@ fun EpisodeCarousel(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
+ val commentsText = stringResource(Lang.episode_comments)
LaunchedEffect(state.playingEpisode) {
val index = state.episodes.indexOf(state.playingEpisode)
if (index == -1) return@LaunchedEffect
@@ -158,7 +164,12 @@ fun EpisodeCarousel(
},
title = {
Text(
- collection.episodeInfo.nameCn.ifEmpty { "第 ${collection.episodeInfo.sort} 话" },
+ collection.episodeInfo.nameCn.ifEmpty {
+ stringResource(
+ Lang.subject_episode_default_title,
+ collection.episodeInfo.sort.toString(),
+ )
+ },
color = if (isPlaying) MaterialTheme.colorScheme.primary else LocalContentColor.current,
)
},
@@ -180,7 +191,7 @@ fun EpisodeCarousel(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
- Icon(Icons.AutoMirrored.Outlined.Chat, contentDescription = "评论数量")
+ Icon(Icons.AutoMirrored.Outlined.Chat, contentDescription = commentsText)
Text(collection.episodeInfo.comment.toString(), softWrap = false)
}
@@ -227,6 +238,7 @@ private fun EpisodeCacheStatusLabel(
state: EpisodeCarouselState,
episode: EpisodeCollectionInfo,
) {
+ val cachedText = stringResource(Lang.subject_episode_cached)
val cacheStatusState by remember(state, episode) {
derivedStateOf { state.cacheStatus(episode) }
}
@@ -253,7 +265,7 @@ private fun EpisodeCacheStatusLabel(
when (cacheStatusState) {
is EpisodeCacheStatus.Cached -> {
Icon(Icons.Rounded.DownloadDone, contentDescription = null)
- Text("已缓存", softWrap = false)
+ Text(cachedText, softWrap = false)
}
is EpisodeCacheStatus.Caching -> {
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeDetails.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeDetails.kt
index 56bae13c98..1ad13e2b9a 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeDetails.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeDetails.kt
@@ -108,6 +108,18 @@ import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.paddingIfNotEmpty
import me.him188.ani.app.ui.foundation.widgets.ModalSideSheet
import me.him188.ani.app.ui.foundation.widgets.rememberModalSideSheetState
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_danmaku_cancel
+import me.him188.ani.app.ui.lang.settings_danmaku_confirm
+import me.him188.ani.app.ui.lang.subject_episode_close_selector
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_time_shift_current_offset
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_time_shift_description
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_time_shift_reset
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_time_shift_restore
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_time_shift_title
+import me.him188.ani.app.ui.lang.subject_episode_related_recommendations
+import me.him188.ani.app.ui.lang.subject_episode_select_media_source
+import me.him188.ani.app.ui.lang.subject_episode_wish_change_to
import me.him188.ani.app.ui.mediafetch.MediaSelectorState
import me.him188.ani.app.ui.mediafetch.MediaSelectorView
import me.him188.ani.app.ui.mediafetch.MediaSourceResultListPresentation
@@ -154,6 +166,7 @@ import me.him188.ani.utils.analytics.AnalyticsEvent.Companion.SubjectEnter
import me.him188.ani.utils.analytics.AnalyticsEvent.Companion.SubjectRecommendationClick
import me.him188.ani.utils.analytics.recordEvent
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
import kotlin.math.roundToLong
@Stable
@@ -327,7 +340,8 @@ fun EpisodeDetails(
UnifiedCollectionType.WISH, UnifiedCollectionType.ON_HOLD -> {
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
Text(
- "已想看,可更改为:", Modifier.align(Alignment.CenterVertically),
+ stringResource(Lang.subject_episode_wish_change_to),
+ Modifier.align(Alignment.CenterVertically),
)
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { // 一起换行
@@ -362,7 +376,7 @@ fun EpisodeDetails(
TopAppBar(
title = {
Text(
- "选择数据源",
+ stringResource(Lang.subject_episode_select_media_source),
modifier = Modifier.padding(start = 8.dp),
)
},
@@ -371,7 +385,10 @@ fun EpisodeDetails(
onClick = { sheetState.close() },
modifier = Modifier.padding(end = 8.dp),
) {
- Icon(Icons.Outlined.Close, contentDescription = "关闭选择器")
+ Icon(
+ Icons.Outlined.Close,
+ contentDescription = stringResource(Lang.subject_episode_close_selector),
+ )
}
},
colors = TopAppBarDefaults.topAppBarColors(
@@ -544,7 +561,7 @@ fun EpisodeDetails(
subjectRecommendations = { horizontalPadding ->
item("subject_recommendation_header") {
SectionTitle {
- Text("相关推荐")
+ Text(stringResource(Lang.subject_episode_related_recommendations))
}
}
for (recommendation in subjectRecommendations) {
@@ -680,24 +697,31 @@ private fun DanmakuTimeShiftDialog(
}
val shiftLabel = remember(shift) { formatDanmakuShiftMillis(shift.roundToLong()) }
+ val confirmText = stringResource(Lang.settings_danmaku_confirm)
+ val cancelText = stringResource(Lang.settings_danmaku_cancel)
+ val titleText = stringResource(Lang.subject_episode_danmaku_time_shift_title, serviceName)
+ val descriptionText = stringResource(Lang.subject_episode_danmaku_time_shift_description)
+ val currentOffsetText = stringResource(Lang.subject_episode_danmaku_time_shift_current_offset, shiftLabel)
+ val resetText = stringResource(Lang.subject_episode_danmaku_time_shift_reset)
+ val restoreText = stringResource(Lang.subject_episode_danmaku_time_shift_restore)
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = { onConfirm(shift.roundToLong()) }) {
- Text("确定")
+ Text(confirmText)
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
- Text("取消")
+ Text(cancelText)
}
},
- title = { Text("$serviceName 弹幕时间校准") },
+ title = { Text(titleText) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
- Text("调整弹幕出现时间以匹配当前视频。正值表示弹幕延后,负值表示弹幕提前。")
- Text("当前偏移:$shiftLabel")
+ Text(descriptionText)
+ Text(currentOffsetText)
Slider(
value = shift,
onValueChange = { shift = it.coerceIn(sliderRange.start, sliderRange.endInclusive) },
@@ -717,14 +741,14 @@ private fun DanmakuTimeShiftDialog(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
OutlinedButton(onClick = { shift = 0f }) {
- Text("重置为 0")
+ Text(resetText)
}
OutlinedButton(
onClick = {
shift = currentShiftMillis.toFloat().coerceIn(sliderRange.start, sliderRange.endInclusive)
},
) {
- Text("恢复原值")
+ Text(restoreText)
}
}
}
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeListSection.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeListSection.kt
index c585e2aea4..a5d49ba650 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeListSection.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/EpisodeListSection.kt
@@ -44,7 +44,6 @@ import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
@@ -80,6 +79,12 @@ import me.him188.ani.app.ui.foundation.icons.PlayingIcon
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
import me.him188.ani.app.ui.foundation.theme.AniTheme
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.episode_danmaku_match_select_episode
+import me.him188.ani.app.ui.lang.subject_episode_collapse
+import me.him188.ani.app.ui.lang.subject_episode_episode_list
+import me.him188.ani.app.ui.lang.subject_episode_expand
+import me.him188.ani.app.ui.lang.subject_episode_view_more_episodes
import me.him188.ani.app.ui.subject.AiringLabel
import me.him188.ani.app.ui.subject.AiringLabelState
import me.him188.ani.app.ui.subject.createTestAiringLabelState
@@ -88,6 +93,7 @@ import me.him188.ani.app.ui.subject.episode.details.components.PaginatedEpisodeL
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
import me.him188.ani.datasources.api.topic.isDoneOrDropped
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
/**
* 剧集列表区域组件,根据屏幕尺寸自适应显示不同的UI布局。
@@ -146,6 +152,9 @@ private fun WideEpisodeListSection(
modifier: Modifier = Modifier,
onToggleExpanded: () -> Unit,
) {
+ val episodeListText = stringResource(Lang.subject_episode_episode_list)
+ val collapseText = stringResource(Lang.subject_episode_collapse)
+ val expandText = stringResource(Lang.subject_episode_expand)
Box(modifier = modifier.padding(horizontal = 16.dp).fillMaxWidth()) {
Column {
AnimatedVisibility(
@@ -226,7 +235,7 @@ private fun WideEpisodeListSection(
ListItem(
headlineContent = {
Text(
- "选集",
+ episodeListText,
style = MaterialTheme.typography.titleMedium,
)
},
@@ -239,7 +248,7 @@ private fun WideEpisodeListSection(
trailingContent = {
Icon(
if (expanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore,
- contentDescription = if (expanded) "收起" else "展开",
+ contentDescription = if (expanded) collapseText else expandText,
)
},
colors = ListItemDefaults.colors(
@@ -265,6 +274,9 @@ private fun NarrowEpisodeListSection(
airingLabelState: AiringLabelState,
modifier: Modifier = Modifier,
) {
+ val episodeListText = stringResource(Lang.subject_episode_episode_list)
+ val viewMoreEpisodesText = stringResource(Lang.subject_episode_view_more_episodes)
+ val selectEpisodeText = stringResource(Lang.episode_danmaku_match_select_episode)
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val horizontalListState = rememberLazyListState()
@@ -279,7 +291,7 @@ private fun NarrowEpisodeListSection(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
- "选集",
+ episodeListText,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.combinedClickable(
onClick = {},
@@ -311,7 +323,7 @@ private fun NarrowEpisodeListSection(
Spacer(Modifier.width(4.dp))
Icon(
Icons.Outlined.ChevronRight,
- contentDescription = "查看更多剧集",
+ contentDescription = viewMoreEpisodesText,
Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -375,7 +387,7 @@ private fun NarrowEpisodeListSection(
) {
Column {
TopAppBar(
- title = { Text("选择剧集") },
+ title = { Text(selectEpisodeText) },
windowInsets = WindowInsets(0),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = BottomSheetDefaults.ContainerColor,
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/DanmakuMatchInfoGrid.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/DanmakuMatchInfoGrid.kt
index a4c67acc6a..237425b086 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/DanmakuMatchInfoGrid.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/DanmakuMatchInfoGrid.kt
@@ -54,11 +54,26 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import me.him188.ani.app.domain.episode.DanmakuFetchResultWithConfig
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.episode_danmaku_match_change
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_count
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_disabled
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_match_exact
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_match_fuzzy
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_match_none
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_match_semi_fuzzy
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_service_bilibili
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_service_dandanplay
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_settings_for
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_time_shift_item
+import me.him188.ani.app.ui.lang.subject_episode_disable
+import me.him188.ani.app.ui.lang.subject_episode_enable
import me.him188.ani.danmaku.api.DanmakuServiceId
import me.him188.ani.danmaku.api.provider.DanmakuMatchInfo
import me.him188.ani.danmaku.api.provider.DanmakuMatchMethod
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.format1f
+import org.jetbrains.compose.resources.stringResource
import kotlin.math.abs
@Composable
@@ -109,6 +124,10 @@ fun DanmakuSourceCard(
dropdown: @Composable () -> Unit = {},
colors: CardColors = CardDefaults.cardColors(),
) {
+ val serviceName = renderDanmakuServiceId(info.serviceId)
+ val settingsText = stringResource(Lang.subject_episode_danmaku_settings_for, serviceName)
+ val danmakuCountText = stringResource(Lang.subject_episode_danmaku_count)
+ val disabledText = stringResource(Lang.subject_episode_danmaku_disabled)
Card(onClick, modifier, colors = colors) {
Column(
Modifier.padding(bottom = 16.dp),
@@ -128,7 +147,7 @@ fun DanmakuSourceCard(
trailingContent = {
Box {
IconButton(onClickSettings, Modifier.offset(x = 8.dp)) {
- Icon(Icons.Rounded.MoreVert, "设置 ${renderDanmakuServiceId(info.serviceId)}")
+ Icon(Icons.Rounded.MoreVert, settingsText)
}
dropdown()
}
@@ -162,7 +181,7 @@ fun DanmakuSourceCard(
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
- Icon(Icons.Outlined.Subtitles, "弹幕数量")
+ Icon(Icons.Outlined.Subtitles, danmakuCountText)
Text(remember(info.count) { "${info.count}" }, softWrap = false)
}
}
@@ -178,7 +197,7 @@ fun DanmakuSourceCard(
}
} else {
ListItem(
- headlineContent = { Text("已禁用") },
+ headlineContent = { Text(disabledText) },
leadingContent = {
Icon(Icons.Rounded.Close, null)
},
@@ -191,12 +210,13 @@ fun DanmakuSourceCard(
}
}
+@Composable
internal fun renderDanmakuServiceId(serviceId: DanmakuServiceId): String = when (serviceId) {
DanmakuServiceId.Animeko -> "Animeko"
DanmakuServiceId.AcFun -> "AcFun"
DanmakuServiceId.Baha -> "Baha"
- DanmakuServiceId.Bilibili -> "哔哩哔哩"
- DanmakuServiceId.Dandanplay -> "弹弹"
+ DanmakuServiceId.Bilibili -> stringResource(Lang.subject_episode_danmaku_service_bilibili)
+ DanmakuServiceId.Dandanplay -> stringResource(Lang.subject_episode_danmaku_service_dandanplay)
DanmakuServiceId.Tucao -> "Tucao"
// `else` should not reach in production
@@ -226,23 +246,30 @@ fun DanmakuSourceSettingsDropdown(
onClickAdjustShift: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val changeText = stringResource(Lang.episode_danmaku_match_change)
+ val enableText = stringResource(Lang.subject_episode_enable)
+ val disableText = stringResource(Lang.subject_episode_disable)
+ val timeShiftText = stringResource(
+ Lang.subject_episode_danmaku_time_shift_item,
+ formatDanmakuShiftMillis(currentShiftMillis),
+ )
DropdownMenu(expanded, onDismissRequest, modifier) {
DropdownMenuItem(
- text = { Text("更换") },
+ text = { Text(changeText) },
onClick = {
onClickChange()
onDismissRequest()
},
)
DropdownMenuItem(
- text = { Text(if (enabled) "禁用" else "启用") },
+ text = { Text(if (enabled) disableText else enableText) },
onClick = {
onSetEnabled(!enabled)
onDismissRequest()
},
)
DropdownMenuItem(
- text = { Text("时间校准 (${formatDanmakuShiftMillis(currentShiftMillis)})") },
+ text = { Text(timeShiftText) },
leadingIcon = { Icon(Icons.Outlined.Schedule, null) },
onClick = {
onClickAdjustShift()
@@ -258,6 +285,9 @@ private fun DanmakuMatchMethodView(
showDetails: Boolean,
modifier: Modifier = Modifier,
) {
+ val semiFuzzyMatchText = stringResource(Lang.subject_episode_danmaku_match_semi_fuzzy)
+ val fuzzyMatchText = stringResource(Lang.subject_episode_danmaku_match_fuzzy)
+ val noMatchText = stringResource(Lang.subject_episode_danmaku_match_none)
Column(modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) {
when (method) {
is DanmakuMatchMethod.Exact -> {
@@ -279,7 +309,7 @@ private fun DanmakuMatchMethodView(
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
Icon(Icons.Outlined.QuestionMark, null)
- Text("半模糊匹配", softWrap = false)
+ Text(semiFuzzyMatchText, softWrap = false)
}
}
if (showDetails) {
@@ -299,7 +329,7 @@ private fun DanmakuMatchMethodView(
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
Icon(Icons.Outlined.QuestionMark, null)
- Text("模糊匹配", softWrap = false)
+ Text(fuzzyMatchText, softWrap = false)
}
}
if (showDetails) {
@@ -331,7 +361,7 @@ private fun DanmakuMatchMethodView(
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) {
Icon(Icons.Outlined.Close, null)
- Text("无匹配", softWrap = false)
+ Text(noMatchText, softWrap = false)
}
}
}
@@ -342,13 +372,14 @@ private fun DanmakuMatchMethodView(
@Composable
private fun ExactMatch() {
+ val exactMatchText = stringResource(Lang.subject_episode_danmaku_match_exact)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
Icon(Icons.Outlined.WorkspacePremium, null)
- Text("精确匹配")
+ Text(exactMatchText)
}
}
}
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/EpisodeWatchStatusButton.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/EpisodeWatchStatusButton.kt
index a44f46ce27..9ce8266f88 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/EpisodeWatchStatusButton.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/EpisodeWatchStatusButton.kt
@@ -15,6 +15,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_watched
+import org.jetbrains.compose.resources.stringResource
@Composable
@@ -25,6 +28,7 @@ fun EpisodeWatchStatusButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
+ val watchedText = stringResource(Lang.subject_episode_watched)
Box(
modifier,
contentAlignment = Alignment.CenterEnd,
@@ -41,7 +45,7 @@ fun EpisodeWatchStatusButton(
SuggestionChip(
onClick = onMarkAsDone,
label = {
- Text("看过", softWrap = false)
+ Text(watchedText, softWrap = false)
},
icon = {
Icon(Icons.Outlined.AddTask, null)
@@ -55,4 +59,3 @@ fun EpisodeWatchStatusButton(
}
}
}
-
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/PlayingEpisodeItem.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/PlayingEpisodeItem.kt
index 5f307df6e5..5a470cf1f6 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/PlayingEpisodeItem.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/PlayingEpisodeItem.kt
@@ -68,6 +68,12 @@ import me.him188.ani.app.ui.foundation.icons.PlayingIcon
import me.him188.ani.app.ui.foundation.layout.paddingIfNotEmpty
import me.him188.ani.app.ui.foundation.text.ProvideContentColor
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_unknown
+import me.him188.ani.app.ui.lang.episode_summary_share
+import me.him188.ani.app.ui.lang.subject_episode_cache
+import me.him188.ani.app.ui.lang.subject_episode_select_media_source
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.media.renderProperties
import me.him188.ani.app.ui.settings.rendering.MediaSourceIcons
import me.him188.ani.app.ui.subject.episode.statistics.VideoLoadingSummary
@@ -76,6 +82,7 @@ import me.him188.ani.datasources.api.Media
import me.him188.ani.datasources.api.source.MediaSourceInfo
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.isAndroid
+import org.jetbrains.compose.resources.stringResource
/**
* 剧集详情页中的正在播放的剧集卡片. 需要放在合适的 `Card` 中.
@@ -242,6 +249,7 @@ private fun PreviewEpisodeItemImpl(
filename: String? = "filename-".repeat(3) + ".mkv",
videoLoadingState: VideoLoadingState = VideoLoadingState.Succeed(false),
) {
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
Card(
modifier = Modifier
.padding(horizontal = 16.dp)
@@ -254,7 +262,7 @@ private fun PreviewEpisodeItemImpl(
mediaSelected = media != null,
mediaLabels = {
media?.let {
- Text(media.renderProperties())
+ Text(media.renderProperties(mediaDetailsStrings))
}
},
filename = {
@@ -332,8 +340,9 @@ object PlayingEpisodeItemDefaults {
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val cacheText = stringResource(Lang.subject_episode_cache)
IconButton(onClick, modifier) {
- Icon(Icons.Rounded.Download, "缓存")
+ Icon(Icons.Rounded.Download, cacheText)
}
}
@@ -343,9 +352,10 @@ object PlayingEpisodeItemDefaults {
modifier: Modifier = Modifier,
) {
var showShareDropdown by rememberSaveable { mutableStateOf(false) }
+ val shareText = stringResource(Lang.episode_summary_share)
Box {
IconButton({ showShareDropdown = true }, modifier) {
- Icon(Icons.Rounded.Outbox, "分享")
+ Icon(Icons.Rounded.Outbox, shareText)
}
ShareEpisodeDropdown(
data, showShareDropdown,
@@ -362,6 +372,8 @@ object PlayingEpisodeItemDefaults {
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val unknownText = stringResource(Lang.cache_unknown)
+ val selectMediaSourceText = stringResource(Lang.subject_episode_select_media_source)
Row(modifier, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
if (media != null) {
OutlinedButton(
@@ -372,7 +384,7 @@ object PlayingEpisodeItemDefaults {
Icon(MediaSourceIcons.location(media.location, media.kind), null)
Text(
- mediaSourceInfo?.displayName ?: "未知",
+ mediaSourceInfo?.displayName ?: unknownText,
Modifier.padding(start = 12.dp).align(Alignment.CenterVertically),
maxLines = 1,
softWrap = false,
@@ -386,7 +398,7 @@ object PlayingEpisodeItemDefaults {
Icon(Icons.Rounded.DisplaySettings, null)
Text(
- "选择数据源",
+ selectMediaSourceText,
Modifier.padding(start = 12.dp).align(Alignment.CenterVertically),
maxLines = 1,
softWrap = false,
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/ShareEpisodeDropdown.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/ShareEpisodeDropdown.kt
index f38724d48e..3873405efa 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/ShareEpisodeDropdown.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/details/components/ShareEpisodeDropdown.kt
@@ -27,8 +27,20 @@ import me.him188.ani.app.ui.episode.share.MediaShareData
import me.him188.ani.app.ui.foundation.LocalPlatform
import me.him188.ani.app.ui.foundation.rememberAsyncHandler
import me.him188.ani.app.ui.foundation.setClipEntryText
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_share_copy_link
+import me.him188.ani.app.ui.lang.subject_episode_share_copy_source_page
+import me.him188.ani.app.ui.lang.subject_episode_share_local_file_link
+import me.him188.ani.app.ui.lang.subject_episode_share_magnet_link
+import me.him188.ani.app.ui.lang.subject_episode_share_open_link
+import me.him188.ani.app.ui.lang.subject_episode_share_open_source_page
+import me.him188.ani.app.ui.lang.subject_episode_share_open_with_other_app
+import me.him188.ani.app.ui.lang.subject_episode_share_stream_link
+import me.him188.ani.app.ui.lang.subject_episode_share_torrent_download_link
+import me.him188.ani.app.ui.lang.subject_episode_share_webpage_link
import me.him188.ani.datasources.api.topic.ResourceLocation
import me.him188.ani.utils.platform.isAndroid
+import org.jetbrains.compose.resources.stringResource
@Composable
fun ShareEpisodeDropdown(
@@ -50,15 +62,18 @@ fun ShareEpisodeDropdown(
) {
data.download?.let { download ->
val downloadText = when (download) {
- is ResourceLocation.HttpStreamingFile -> "视频流链接"
- is ResourceLocation.HttpTorrentFile -> "种子文件下载链接"
- is ResourceLocation.LocalFile -> "本地文件链接"
- is ResourceLocation.MagnetLink -> "磁力链接"
- is ResourceLocation.WebVideo -> "网页链接" // should not happen though
+ is ResourceLocation.HttpStreamingFile -> stringResource(Lang.subject_episode_share_stream_link)
+ is ResourceLocation.HttpTorrentFile -> stringResource(Lang.subject_episode_share_torrent_download_link)
+ is ResourceLocation.LocalFile -> stringResource(Lang.subject_episode_share_local_file_link)
+ is ResourceLocation.MagnetLink -> stringResource(Lang.subject_episode_share_magnet_link)
+ is ResourceLocation.WebVideo -> stringResource(Lang.subject_episode_share_webpage_link) // should not happen though
}
+ val copyDownloadText = stringResource(Lang.subject_episode_share_copy_link, downloadText)
+ val openDownloadText = stringResource(Lang.subject_episode_share_open_link, downloadText)
+ val openWithOtherAppText = stringResource(Lang.subject_episode_share_open_with_other_app)
DropdownMenuItem(
text = {
- Text("复制$downloadText")
+ Text(copyDownloadText)
},
onClick = {
onDismissRequest()
@@ -69,7 +84,7 @@ fun ShareEpisodeDropdown(
leadingIcon = { Icon(Icons.Rounded.ContentCopy, null) },
)
DropdownMenuItem(
- text = { Text("访问$downloadText") },
+ text = { Text(openDownloadText) },
onClick = {
onDismissRequest()
uriHandler.openUri(download.uri)
@@ -78,7 +93,7 @@ fun ShareEpisodeDropdown(
)
if (LocalPlatform.current.isAndroid() && download !is ResourceLocation.WebVideo) {
DropdownMenuItem(
- text = { Text("用其他应用打开") },
+ text = { Text(openWithOtherAppText) },
onClick = {
onDismissRequest()
browserNavigator.intentOpenVideo(context, download.uri)
@@ -89,8 +104,10 @@ fun ShareEpisodeDropdown(
}
data.websiteUrl?.let { websiteUrl ->
+ val copySourcePageText = stringResource(Lang.subject_episode_share_copy_source_page)
+ val openSourcePageText = stringResource(Lang.subject_episode_share_open_source_page)
DropdownMenuItem(
- text = { Text("复制数据源页面链接") },
+ text = { Text(copySourcePageText) },
onClick = {
onDismissRequest()
scope.launch {
@@ -100,7 +117,7 @@ fun ShareEpisodeDropdown(
leadingIcon = { Icon(Icons.Rounded.ContentCopy, null) },
)
DropdownMenuItem(
- text = { Text("访问数据源页面") },
+ text = { Text(openSourcePageText) },
onClick = {
onDismissRequest()
uriHandler.openUri(websiteUrl)
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/statistics/VideoLoadingSummary.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/statistics/VideoLoadingSummary.kt
index a1e0ee626e..e1171c6f2a 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/statistics/VideoLoadingSummary.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/statistics/VideoLoadingSummary.kt
@@ -38,6 +38,18 @@ import me.him188.ani.app.ui.foundation.ifThen
import me.him188.ani.app.ui.foundation.rememberAsyncHandler
import me.him188.ani.app.ui.foundation.setClipEntryText
import me.him188.ani.app.ui.foundation.text.ProvideContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_mediasource_close
+import me.him188.ani.app.ui.lang.subject_episode_statistics_cancelled
+import me.him188.ani.app.ui.lang.subject_episode_statistics_copy
+import me.him188.ani.app.ui.lang.subject_episode_statistics_error
+import me.him188.ani.app.ui.lang.subject_episode_statistics_error_details
+import me.him188.ani.app.ui.lang.subject_episode_statistics_network_error
+import me.him188.ani.app.ui.lang.subject_episode_statistics_no_matching_file
+import me.him188.ani.app.ui.lang.subject_episode_statistics_resolution_timed_out
+import me.him188.ani.app.ui.lang.subject_episode_statistics_unknown_error_tap
+import me.him188.ani.app.ui.lang.subject_episode_statistics_unsupported_media
+import org.jetbrains.compose.resources.stringResource
@Composable
@@ -47,6 +59,9 @@ fun SimpleErrorDialog(
) {
val clipboard = LocalClipboard.current
val scope = rememberAsyncHandler()
+ val copyText = stringResource(Lang.subject_episode_statistics_copy)
+ val closeText = stringResource(Lang.settings_mediasource_close)
+ val errorDetailsText = stringResource(Lang.subject_episode_statistics_error_details)
val copy: () -> Unit = {
scope.launch {
clipboard.setClipEntryText(text())
@@ -56,22 +71,22 @@ fun SimpleErrorDialog(
onDismissRequest,
confirmButton = {
TextButton(copy) {
- Text("复制")
+ Text(copyText)
}
},
dismissButton = {
TextButton(onDismissRequest) {
- Text("关闭")
+ Text(closeText)
}
},
- title = { Text("错误详情") },
+ title = { Text(errorDetailsText) },
text = {
OutlinedTextField(
value = text(),
onValueChange = {},
trailingIcon = {
IconButton(copy) {
- Icon(Icons.Outlined.ContentCopy, "复制")
+ Icon(Icons.Outlined.ContentCopy, copyText)
}
},
readOnly = true,
@@ -87,6 +102,13 @@ fun VideoLoadingSummary(
color: Color = MaterialTheme.colorScheme.error,
) {
if (state is VideoLoadingState.Failed) {
+ val errorText = stringResource(Lang.subject_episode_statistics_error)
+ val noMatchingFileText = stringResource(Lang.subject_episode_statistics_no_matching_file)
+ val resolutionTimedOutText = stringResource(Lang.subject_episode_statistics_resolution_timed_out)
+ val unsupportedMediaText = stringResource(Lang.subject_episode_statistics_unsupported_media)
+ val unknownErrorTapText = stringResource(Lang.subject_episode_statistics_unknown_error_tap)
+ val cancelledText = stringResource(Lang.subject_episode_statistics_cancelled)
+ val networkErrorText = stringResource(Lang.subject_episode_statistics_network_error)
ProvideContentColor(color) {
var showErrorDialog by rememberSaveable(state) { mutableStateOf(false) }
if (showErrorDialog) {
@@ -110,20 +132,21 @@ fun VideoLoadingSummary(
contentAlignment = Alignment.Center,
) {
Icon(
- Icons.Outlined.ErrorOutline, "错误",
+ Icons.Outlined.ErrorOutline,
+ errorText,
)
}
when (state) {
- VideoLoadingState.NoMatchingFile -> Text("未匹配到文件")
- VideoLoadingState.ResolutionTimedOut -> Text("解析超时")
- VideoLoadingState.UnsupportedMedia -> Text("不支持的视频类型")
+ VideoLoadingState.NoMatchingFile -> Text(noMatchingFileText)
+ VideoLoadingState.ResolutionTimedOut -> Text(resolutionTimedOutText)
+ VideoLoadingState.UnsupportedMedia -> Text(unsupportedMediaText)
is VideoLoadingState.UnknownError -> {
- Text("未知错误,点击查看")
+ Text(unknownErrorTapText)
}
- VideoLoadingState.Cancelled -> Text("已取消")
- VideoLoadingState.NetworkError -> Text("网络错误,请检查网络连接状况")
+ VideoLoadingState.Cancelled -> Text(cancelledText)
+ VideoLoadingState.NetworkError -> Text(networkErrorText)
}
}
}
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/statistics/VideoStatistics.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/statistics/VideoStatistics.kt
index 775323c0d1..f03c8f326a 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/statistics/VideoStatistics.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/statistics/VideoStatistics.kt
@@ -61,10 +61,22 @@ import me.him188.ani.app.domain.media.selector.MediaSelector
import me.him188.ani.app.domain.player.VideoLoadingState
import me.him188.ani.app.ui.foundation.setClipEntryText
import me.him188.ani.app.ui.foundation.text.ProvideContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_select_media_source
+import me.him188.ani.app.ui.lang.subject_episode_statistics_danmaku_disabled
+import me.him188.ani.app.ui.lang.subject_episode_statistics_danmaku_load_failed_tap
+import me.him188.ani.app.ui.lang.subject_episode_statistics_danmaku_loading
+import me.him188.ani.app.ui.lang.subject_episode_statistics_danmaku_sources_count
+import me.him188.ani.app.ui.lang.subject_episode_statistics_error_message
+import me.him188.ani.app.ui.lang.subject_episode_statistics_now_playing
+import me.him188.ani.app.ui.lang.subject_episode_statistics_show_less
+import me.him188.ani.app.ui.lang.subject_episode_statistics_show_more
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.media.renderProperties
import me.him188.ani.app.ui.mediafetch.MediaSourceInfoProvider
import me.him188.ani.datasources.api.Media
import me.him188.ani.datasources.api.source.MediaSourceInfo
+import org.jetbrains.compose.resources.stringResource
import org.openani.mediamp.MediampPlayer
class VideoStatisticsCollector(
@@ -137,6 +149,11 @@ fun DanmakuMatchInfoSummaryBanner(
modifier: Modifier = Modifier,
) {
val danmakuLoadingState = danmakuStatistics.danmakuLoadingState
+ val danmakuLoadFailedTapText = stringResource(Lang.subject_episode_statistics_danmaku_load_failed_tap)
+ val showLessText = stringResource(Lang.subject_episode_statistics_show_less)
+ val showMoreText = stringResource(Lang.subject_episode_statistics_show_more)
+ val danmakuDisabledText = stringResource(Lang.subject_episode_statistics_danmaku_disabled)
+ val danmakuLoadingText = stringResource(Lang.subject_episode_statistics_danmaku_loading)
var showDialog by rememberSaveable { mutableStateOf(false) }
if (showDialog) {
val text = remember(danmakuLoadingState) {
@@ -176,11 +193,11 @@ fun DanmakuMatchInfoSummaryBanner(
ProvideContentColor(MaterialTheme.colorScheme.error) {
Icon(Icons.Rounded.ErrorOutline, null)
Text(
- "弹幕加载失败,点击查看",
+ danmakuLoadFailedTapText,
Modifier.weight(1f).padding(start = 12.dp),
style = MaterialTheme.typography.titleSmall,
)
- Icon(Icons.Outlined.ChevronRight, "查看错误")
+ Icon(Icons.Outlined.ChevronRight, danmakuLoadFailedTapText, Modifier.size(20.dp))
}
}
@@ -192,17 +209,19 @@ fun DanmakuMatchInfoSummaryBanner(
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
- remember(danmakuStatistics) {
- "${danmakuStatistics.fetchResults.size} 个弹幕源,共计 ${danmakuStatistics.fetchResults.sumOf { it.matchInfo.count }} 条弹幕"
- },
+ stringResource(
+ Lang.subject_episode_statistics_danmaku_sources_count,
+ danmakuStatistics.fetchResults.size,
+ danmakuStatistics.fetchResults.sumOf { it.matchInfo.count },
+ ),
Modifier.weight(1f).padding(start = 12.dp),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (expanded) {
- Icon(Icons.Rounded.ArrowDropUp, "展示更少", Modifier.size(20.dp))
+ Icon(Icons.Rounded.ArrowDropUp, showLessText, Modifier.size(20.dp))
} else {
- Icon(Icons.Outlined.ChevronRight, "展示更多", Modifier.size(20.dp))
+ Icon(Icons.Outlined.ChevronRight, showMoreText, Modifier.size(20.dp))
}
}
@@ -210,13 +229,13 @@ fun DanmakuMatchInfoSummaryBanner(
DanmakuLoadingState.Loading -> {
if (!danmakuStatistics.danmakuEnabled) {
Text(
- "弹幕已关闭,可在播放器内开启",
+ danmakuDisabledText,
Modifier.weight(1f),
style = MaterialTheme.typography.titleSmall,
)
} else {
Text(
- "弹幕装填中",
+ danmakuLoadingText,
Modifier.weight(1f),
softWrap = false,
style = MaterialTheme.typography.titleSmall,
@@ -238,13 +257,14 @@ fun VideoStatistics(
) {
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
when (val loadingState = state.videoLoadingState) {
is VideoLoadingState.Succeed -> {
- val mediaPropertiesText by remember {
+ val mediaPropertiesText by remember(state.playingMedia, mediaDetailsStrings) {
derivedStateOf {
- state.playingMedia?.renderProperties()
+ state.playingMedia?.renderProperties(mediaDetailsStrings)
}
}
NowPlayingLabel(mediaPropertiesText, state.playingFilename)
@@ -277,13 +297,15 @@ private fun NowPlayingLabel(
filename: String?,
modifier: Modifier = Modifier,
) {
+ val nowPlayingText = stringResource(Lang.subject_episode_statistics_now_playing)
+ val selectMediaSourceText = stringResource(Lang.subject_episode_select_media_source)
Row(modifier) {
ProvideTextStyle(MaterialTheme.typography.titleMedium) {
if (playingMedia != null) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row {
Text(
- "正在播放: ",
+ nowPlayingText,
color = MaterialTheme.colorScheme.primary,
)
Text(
@@ -302,7 +324,7 @@ private fun NowPlayingLabel(
}
}
} else {
- Text("请选择数据源")
+ Text(selectMediaSourceText)
}
}
}
@@ -318,7 +340,7 @@ private fun ErrorTextBox(
text,
onValueChange = {},
modifier,
- label = { Text("错误信息") },
+ label = { Text(stringResource(Lang.subject_episode_statistics_error_message)) },
shape = MaterialTheme.shapes.medium,
readOnly = true,
singleLine = true,
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/components/EpisodeVideoSideSheet.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/components/EpisodeVideoSideSheet.kt
index ead7631880..761411e499 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/components/EpisodeVideoSideSheet.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/components/EpisodeVideoSideSheet.kt
@@ -30,6 +30,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import kotlinx.serialization.Serializable
import me.him188.ani.app.ui.foundation.layout.desktopTitleBarPadding
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_close
+import me.him188.ani.app.ui.lang.subject_episode_danmaku_settings_title
import me.him188.ani.app.ui.settings.danmaku.DanmakuRegexFilterState
import me.him188.ani.app.ui.subject.episode.EpisodeVideoDefaults
import me.him188.ani.app.ui.subject.episode.video.settings.EpisodeVideoSettings
@@ -44,6 +47,7 @@ import me.him188.ani.app.videoplayer.ui.hasPageAsState
import me.him188.ani.app.videoplayer.ui.rememberAlwaysOnRequester
import me.him188.ani.app.videoplayer.ui.rememberVideoSideSheetsController
import me.him188.ani.danmaku.ui.DanmakuConfig
+import org.jetbrains.compose.resources.stringResource
/**
* See extensions on [EpisodeVideoSideSheets] for sheet implementations.
@@ -107,16 +111,19 @@ object EpisodeVideoSideSheets {
onDismissRequest: () -> Unit,
onNavigateToFilterSettings: () -> Unit
) {
+ val danmakuSettingsText = stringResource(Lang.subject_episode_danmaku_settings_title)
+ val closeText = stringResource(Lang.subject_episode_close)
+
// 全屏:直接展示主设置 SideSheet
if (expanded) {
val viewModel = remember { EpisodeVideoSettingsViewModel() }
SideSheetLayout(
- title = { Text("弹幕设置") },
+ title = { Text(danmakuSettingsText) },
onDismissRequest = onDismissRequest,
modifier = Modifier,
closeButton = {
IconButton(onClick = onDismissRequest) {
- Icon(Icons.Rounded.Close, contentDescription = "关闭")
+ Icon(Icons.Rounded.Close, contentDescription = closeText)
}
},
) {
@@ -175,13 +182,16 @@ fun EpisodeVideoSideSheets.DanmakuSettingsSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val danmakuSettingsText = stringResource(Lang.subject_episode_danmaku_settings_title)
+ val closeText = stringResource(Lang.subject_episode_close)
+
SideSheetLayout(
- title = { Text(text = "弹幕设置") },
+ title = { Text(text = danmakuSettingsText) },
onDismissRequest = onDismissRequest,
modifier,
closeButton = {
IconButton(onClick = onDismissRequest) {
- Icon(Icons.Rounded.Close, contentDescription = "关闭")
+ Icon(Icons.Rounded.Close, contentDescription = closeText)
}
},
) {
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/loading/EpisodeVideoLoadingIndicator.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/loading/EpisodeVideoLoadingIndicator.kt
index a7da502cc6..19c9c327c5 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/loading/EpisodeVideoLoadingIndicator.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/loading/EpisodeVideoLoadingIndicator.kt
@@ -30,10 +30,28 @@ import me.him188.ani.app.domain.media.player.data.DownloadingMediaData
import me.him188.ani.app.domain.player.VideoLoadingState
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.TextWithBorder
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_auto_selecting
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_buffering
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_buffering_bt_no_speed_try_switch
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_buffering_bt_too_long
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_buffering_too_long
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_cause_cancelled
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_cause_network_error
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_cause_no_matching_file
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_cause_resolution_timed_out
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_cause_unknown_error
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_cause_unsupported_media
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_decoding_bt
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_decoding_data
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_failed_prefix
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_player_error
+import me.him188.ani.app.ui.lang.subject_episode_video_loading_resolving_source
import me.him188.ani.app.videoplayer.ui.VideoLoadingIndicator
import me.him188.ani.datasources.api.topic.FileSize
import me.him188.ani.datasources.api.topic.FileSize.Companion.Unspecified
import me.him188.ani.datasources.api.topic.FileSize.Companion.bytes
+import org.jetbrains.compose.resources.stringResource
import org.openani.mediamp.ExperimentalMediampApi
import org.openani.mediamp.MediampPlayer
import org.openani.mediamp.PlaybackState
@@ -87,25 +105,40 @@ fun EpisodeVideoLoadingIndicator(
playerError: Boolean = false,
modifier: Modifier = Modifier,
) {
+ val playerErrorText = stringResource(Lang.subject_episode_video_loading_player_error)
+ val autoSelectingText = stringResource(Lang.subject_episode_video_loading_auto_selecting)
+ val resolvingSourceText = stringResource(Lang.subject_episode_video_loading_resolving_source)
+ val decodingDataText = stringResource(Lang.subject_episode_video_loading_decoding_data)
+ val decodingBtText = stringResource(Lang.subject_episode_video_loading_decoding_bt)
+ val bufferingText = stringResource(Lang.subject_episode_video_loading_buffering)
+ val bufferingBtTooLongText = stringResource(Lang.subject_episode_video_loading_buffering_bt_too_long)
+ val bufferingNoSpeedTrySwitchText =
+ stringResource(Lang.subject_episode_video_loading_buffering_bt_no_speed_try_switch)
+ val bufferingTooLongText = stringResource(Lang.subject_episode_video_loading_buffering_too_long)
+ val failedPrefix = stringResource(Lang.subject_episode_video_loading_failed_prefix)
+ val causeLabels = VideoLoadingCauseLabels(
+ resolutionTimedOut = stringResource(Lang.subject_episode_video_loading_cause_resolution_timed_out),
+ unknownError = stringResource(Lang.subject_episode_video_loading_cause_unknown_error),
+ unsupportedMedia = stringResource(Lang.subject_episode_video_loading_cause_unsupported_media),
+ noMatchingFile = stringResource(Lang.subject_episode_video_loading_cause_no_matching_file),
+ cancelled = stringResource(Lang.subject_episode_video_loading_cause_cancelled),
+ networkError = stringResource(Lang.subject_episode_video_loading_cause_network_error),
+ )
VideoLoadingIndicator(
showProgress = state is VideoLoadingState.Progressing,
text = {
if (playerError) {
- TextWithBorder("播放失败, 请更换数据源", color = MaterialTheme.colorScheme.error)
+ TextWithBorder(playerErrorText, color = MaterialTheme.colorScheme.error)
return@VideoLoadingIndicator
}
when (state) {
VideoLoadingState.Initial -> {
- if (optimizeForFullscreen) {
- TextWithBorder("正在自动选择数据源,请稍候")
- } else {
- TextWithBorder("正在自动选择数据源,请稍候")
- }
+ TextWithBorder(autoSelectingText)
}
VideoLoadingState.ResolvingSource -> {
TextWithBorder(
- "正在解析资源链接\n通常几秒内完成,否则请切换数据源",
+ resolvingSourceText,
textAlign = TextAlign.Center,
)
}
@@ -113,9 +146,9 @@ fun EpisodeVideoLoadingIndicator(
is VideoLoadingState.DecodingData -> {
TextWithBorder(
if (!state.isBt) {
- "资源解析成功, 正在准备视频"
+ decodingDataText
} else {
- "正在解析磁力链或查询元数据\n通常几秒内完成, 否则请尝试切换数据源或先缓存再看"
+ decodingBtText
},
textAlign = TextAlign.Center,
)
@@ -136,7 +169,7 @@ fun EpisodeVideoLoadingIndicator(
val text by remember {
derivedStateOf {
buildString {
- append("正在缓冲")
+ append(bufferingText)
if (speed != FileSize.Unspecified) {
appendLine()
append(speed.toString())
@@ -146,11 +179,11 @@ fun EpisodeVideoLoadingIndicator(
if (tooLong) {
appendLine()
if (state.isBt) {
- append("BT 初始缓冲耗时稍长, 请耐心等待 30 秒")
+ append(bufferingBtTooLongText)
appendLine()
- append("若持续没有速度, 可尝试切换数据源")
+ append(bufferingNoSpeedTrySwitchText)
} else {
- append("缓冲耗时过长, 可尝试切换数据源")
+ append(bufferingTooLongText)
}
}
}
@@ -162,7 +195,7 @@ fun EpisodeVideoLoadingIndicator(
is VideoLoadingState.Failed -> {
TextWithBorder(
- "加载失败: ${renderCause(state)}",
+ "$failedPrefix${renderCause(state, causeLabels)}",
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
)
@@ -173,13 +206,22 @@ fun EpisodeVideoLoadingIndicator(
)
}
-fun renderCause(cause: VideoLoadingState.Failed): String = when (cause) {
- is VideoLoadingState.ResolutionTimedOut -> "解析超时"
- is VideoLoadingState.UnknownError -> "未知错误"
- is VideoLoadingState.UnsupportedMedia -> "不支持该文件类型"
- VideoLoadingState.NoMatchingFile -> "未找到可播放的文件"
- VideoLoadingState.Cancelled -> "已取消"
- VideoLoadingState.NetworkError -> "网络错误"
+private data class VideoLoadingCauseLabels(
+ val resolutionTimedOut: String,
+ val unknownError: String,
+ val unsupportedMedia: String,
+ val noMatchingFile: String,
+ val cancelled: String,
+ val networkError: String,
+)
+
+private fun renderCause(cause: VideoLoadingState.Failed, labels: VideoLoadingCauseLabels): String = when (cause) {
+ is VideoLoadingState.ResolutionTimedOut -> labels.resolutionTimedOut
+ is VideoLoadingState.UnknownError -> labels.unknownError
+ is VideoLoadingState.UnsupportedMedia -> labels.unsupportedMedia
+ VideoLoadingState.NoMatchingFile -> labels.noMatchingFile
+ VideoLoadingState.Cancelled -> labels.cancelled
+ VideoLoadingState.NetworkError -> labels.networkError
}
@Preview(name = "Selecting Media")
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/settings/EpisodeVideoSettings.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/settings/EpisodeVideoSettings.kt
index 8a4a16cb15..9cdbd3ed0c 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/settings/EpisodeVideoSettings.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/settings/EpisodeVideoSettings.kt
@@ -42,6 +42,32 @@ import me.him188.ani.app.data.repository.user.SettingsRepository
import me.him188.ani.app.ui.foundation.LocalPlatform
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.rememberDebugSettingsViewModel
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_bottom
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_colorful
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_debug_mode
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_density
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_density_dense
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_density_medium
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_density_sparse
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_display_area
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_display_area_full
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_display_area_half
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_display_area_off
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_display_area_one_eighth
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_display_area_one_quarter
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_display_area_one_sixth
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_display_area_three_quarters
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_enable_regex_filter
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_floating
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_font_size
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_font_weight
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_manage_regex_filter
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_opacity
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_speed
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_speed_description
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_stroke_width
+import me.him188.ani.app.ui.lang.subject_episode_video_settings_top
import me.him188.ani.app.ui.settings.SettingsTab
import me.him188.ani.app.ui.settings.framework.AbstractSettingsViewModel
import me.him188.ani.app.ui.settings.framework.SettingsState
@@ -52,6 +78,7 @@ import me.him188.ani.app.ui.settings.framework.components.TextItem
import me.him188.ani.danmaku.ui.DanmakuConfig
import me.him188.ani.danmaku.ui.DanmakuStyle
import me.him188.ani.utils.platform.isDesktop
+import org.jetbrains.compose.resources.stringResource
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.math.roundToInt
@@ -119,6 +146,32 @@ fun EpisodeVideoSettings(
modifier: Modifier = Modifier,
useThinSlider: Boolean = true
) {
+ val topText = stringResource(Lang.subject_episode_video_settings_top)
+ val floatingText = stringResource(Lang.subject_episode_video_settings_floating)
+ val bottomText = stringResource(Lang.subject_episode_video_settings_bottom)
+ val colorfulText = stringResource(Lang.subject_episode_video_settings_colorful)
+ val fontSizeText = stringResource(Lang.subject_episode_video_settings_font_size)
+ val opacityText = stringResource(Lang.subject_episode_video_settings_opacity)
+ val strokeWidthText = stringResource(Lang.subject_episode_video_settings_stroke_width)
+ val fontWeightText = stringResource(Lang.subject_episode_video_settings_font_weight)
+ val speedText = stringResource(Lang.subject_episode_video_settings_speed)
+ val speedDescriptionText = stringResource(Lang.subject_episode_video_settings_speed_description)
+ val densityText = stringResource(Lang.subject_episode_video_settings_density)
+ val denseText = stringResource(Lang.subject_episode_video_settings_density_dense)
+ val mediumText = stringResource(Lang.subject_episode_video_settings_density_medium)
+ val sparseText = stringResource(Lang.subject_episode_video_settings_density_sparse)
+ val displayAreaText = stringResource(Lang.subject_episode_video_settings_display_area)
+ val displayAreaOffText = stringResource(Lang.subject_episode_video_settings_display_area_off)
+ val displayAreaOneEighthText = stringResource(Lang.subject_episode_video_settings_display_area_one_eighth)
+ val displayAreaOneSixthText = stringResource(Lang.subject_episode_video_settings_display_area_one_sixth)
+ val displayAreaOneQuarterText = stringResource(Lang.subject_episode_video_settings_display_area_one_quarter)
+ val displayAreaHalfText = stringResource(Lang.subject_episode_video_settings_display_area_half)
+ val displayAreaThreeQuartersText = stringResource(Lang.subject_episode_video_settings_display_area_three_quarters)
+ val displayAreaFullText = stringResource(Lang.subject_episode_video_settings_display_area_full)
+ val enableRegexFilterText = stringResource(Lang.subject_episode_video_settings_enable_regex_filter)
+ val manageRegexFilterText = stringResource(Lang.subject_episode_video_settings_manage_regex_filter)
+ val debugModeText = stringResource(Lang.subject_episode_video_settings_debug_mode)
+
SettingsTab(modifier.verticalScroll(rememberScrollState())) {
Column {
Surface(Modifier.fillMaxWidth(), color = SettingsDefaults.groupBackgroundColor) {
@@ -133,12 +186,12 @@ fun EpisodeVideoSettings(
if (danmakuConfig.enableTop) Icon(Icons.Rounded.Check, contentDescription = null)
else Icon(Icons.Rounded.Close, contentDescription = null)
},
- label = { Text("顶部", maxLines = 1) },
+ label = { Text(topText, maxLines = 1) },
)
ElevatedFilterChip(
selected = danmakuConfig.enableFloating,
onClick = { setDanmakuConfig { config -> config.copy(enableFloating = !config.enableFloating) } },
- label = { Text("滚动", maxLines = 1) },
+ label = { Text(floatingText, maxLines = 1) },
leadingIcon = {
if (danmakuConfig.enableFloating) Icon(Icons.Rounded.Check, contentDescription = null)
else Icon(Icons.Rounded.Close, contentDescription = null)
@@ -147,7 +200,7 @@ fun EpisodeVideoSettings(
ElevatedFilterChip(
selected = danmakuConfig.enableBottom,
onClick = { setDanmakuConfig { config -> config.copy(enableBottom = !config.enableBottom) } },
- label = { Text("底部", maxLines = 1) },
+ label = { Text(bottomText, maxLines = 1) },
leadingIcon = {
if (danmakuConfig.enableBottom) Icon(Icons.Rounded.Check, contentDescription = null)
else Icon(Icons.Rounded.Close, contentDescription = null)
@@ -162,7 +215,7 @@ fun EpisodeVideoSettings(
if (danmakuConfig.enableColor) Icon(Icons.Rounded.Check, contentDescription = null)
else Icon(Icons.Rounded.Close, contentDescription = null)
},
- label = { Text("彩色", maxLines = 1) },
+ label = { Text(colorfulText, maxLines = 1) },
)
}
}
@@ -177,7 +230,7 @@ fun EpisodeVideoSettings(
},
valueRange = 0.50f..3f,
// steps = ((3f - 0.50f) / 0.05f).toInt() - 1,
- title = { Text("弹幕字号") },
+ title = { Text(fontSizeText) },
valueLabel = { Text(remember(fontSize) { "${(fontSize * 100).roundToInt()}%" }) },
useThinSlider = useThinSlider,
)
@@ -193,7 +246,7 @@ fun EpisodeVideoSettings(
},
valueRange = 0f..1f,
// steps = ((1f - 0f) / 0.05f).toInt() - 1,
- title = { Text("不透明度") },
+ title = { Text(opacityText) },
valueLabel = { Text(remember(alpha) { "${(alpha * 100).roundToInt()}%" }) },
useThinSlider = useThinSlider,
)
@@ -209,7 +262,7 @@ fun EpisodeVideoSettings(
},
valueRange = 0f..2f,
// steps = ((2f - 0f) / 0.1f).toInt() - 1,
- title = { Text("描边宽度") },
+ title = { Text(strokeWidthText) },
valueLabel = { Text(remember(strokeWidth) { "${(strokeWidth * 100).roundToInt()}%" }) },
useThinSlider = useThinSlider,
)
@@ -231,7 +284,7 @@ fun EpisodeVideoSettings(
},
valueRange = 100f..900f,
// steps = ((900 - 100) / 100) - 1,
- title = { Text("弹幕字重") },
+ title = { Text(fontWeightText) },
valueLabel = { Text(remember(fontWeight) { "${fontWeight.toInt()}" }) },
useThinSlider = useThinSlider,
)
@@ -248,8 +301,8 @@ fun EpisodeVideoSettings(
},
valueRange = 0.2f..3f,
// steps = ((3f - 0.2f) / 0.1f).toInt() - 1,
- title = { Text("弹幕速度") },
- description = { Text("弹幕速度不会跟随视频倍速变化") },
+ title = { Text(speedText) },
+ description = { Text(speedDescriptionText) },
valueLabel = { Text(remember(speed) { "${(speed * 100).roundToInt()}%" }) },
useThinSlider = useThinSlider,
)
@@ -284,12 +337,12 @@ fun EpisodeVideoSettings(
},
valueRange = 0f..10f,
steps = 9,
- title = { Text("同屏密度") },
+ title = { Text(densityText) },
valueLabel = {
when (displayDensity.toInt()) {
- in 7..10 -> Text("密集")
- in 4..6 -> Text("适中")
- in 0..3 -> Text("稀疏")
+ in 7..10 -> Text(denseText)
+ in 4..6 -> Text(mediumText)
+ in 0..3 -> Text(sparseText)
}
},
useThinSlider = useThinSlider,
@@ -302,17 +355,17 @@ fun EpisodeVideoSettings(
setDanmakuConfig { config -> config.copy(displayArea = newValue.coerceIn(0f, 1f)) }
},
valueRange = 0f..1f,
- title = { Text("显示区域") },
+ title = { Text(displayAreaText) },
valueLabel = {
val v = danmakuConfig.displayArea
when {
- v == 0f -> Text("关闭")
- v <= 1 / 8f -> Text("1/8 屏")
- v <= 1 / 6f -> Text("1/6 屏")
- v <= 1 / 4f -> Text("1/4 屏")
- v <= 1 / 2f -> Text("半屏")
- v <= 3 / 4f -> Text("3/4 屏")
- v == 1f -> Text("全屏")
+ v == 0f -> Text(displayAreaOffText)
+ v <= 1 / 8f -> Text(displayAreaOneEighthText)
+ v <= 1 / 6f -> Text(displayAreaOneSixthText)
+ v <= 1 / 4f -> Text(displayAreaOneQuarterText)
+ v <= 1 / 2f -> Text(displayAreaHalfText)
+ v <= 3 / 4f -> Text(displayAreaThreeQuartersText)
+ v == 1f -> Text(displayAreaFullText)
}
},
useThinSlider = useThinSlider,
@@ -323,13 +376,13 @@ fun EpisodeVideoSettings(
onCheckedChange = {
switchDanmakuRegexFilterCompletely()
},
- title = { Text("启用正则弹幕过滤器") },
+ title = { Text(enableRegexFilterText) },
)
TextItem(
onClick = { onManageRegexFilters() },
) {
- Text("管理正则弹幕过滤器")
+ Text(manageRegexFilterText)
}
val debugViewModel = rememberDebugSettingsViewModel()
@@ -340,7 +393,7 @@ fun EpisodeVideoSettings(
onCheckedChange = { checked ->
setDanmakuConfig { config -> config.copy(isDebug = checked) }
},
- title = { Text("弹幕调试模式") },
+ title = { Text(debugModeText) },
)
val debugSettings by debugViewModel.debugSettings
@@ -396,4 +449,3 @@ private fun PreviewEpisodeVideoSettingsSideSheet() = ProvideCompositionLocalsFor
}
}
}
-
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EditDanmakuRegexFilterSideSheet.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EditDanmakuRegexFilterSideSheet.kt
index b557a9c885..711a31c9a2 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EditDanmakuRegexFilterSideSheet.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EditDanmakuRegexFilterSideSheet.kt
@@ -51,12 +51,22 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.him188.ani.app.data.models.danmaku.DanmakuRegexFilter
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_close
+import me.him188.ani.app.ui.lang.subject_episode_regex_filter_add
+import me.him188.ani.app.ui.lang.subject_episode_regex_filter_delete
+import me.him188.ani.app.ui.lang.subject_episode_regex_filter_example
+import me.him188.ani.app.ui.lang.subject_episode_regex_filter_invalid
+import me.him188.ani.app.ui.lang.subject_episode_regex_filter_label
+import me.him188.ani.app.ui.lang.subject_episode_regex_filter_management
+import me.him188.ani.app.ui.lang.subject_episode_regex_filter_placeholder
import me.him188.ani.app.ui.settings.danmaku.DanmakuRegexFilterState
import me.him188.ani.app.ui.settings.danmaku.createTestDanmakuRegexFilterState
import me.him188.ani.app.ui.settings.danmaku.isValidRegex
import me.him188.ani.app.ui.subject.episode.video.settings.SideSheetLayout
import me.him188.ani.utils.platform.Uuid
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
@Suppress("UnusedReceiverParameter")
@@ -74,6 +84,12 @@ fun DanmakuRegexFilterContent(
val isBlank by remember { derivedStateOf { input.isBlank() } }
val valid by remember { derivedStateOf { isValidRegex(input) } }
var isError by remember { mutableStateOf(false) }
+ val placeholderText = stringResource(Lang.subject_episode_regex_filter_placeholder)
+ val regexExpressionText = stringResource(Lang.subject_episode_regex_filter_label)
+ val invalidRegexText = stringResource(Lang.subject_episode_regex_filter_invalid)
+ val exampleText = stringResource(Lang.subject_episode_regex_filter_example)
+ val addText = stringResource(Lang.subject_episode_regex_filter_add)
+ val deleteText = stringResource(Lang.subject_episode_regex_filter_delete)
val isPortrait = !expanded
@@ -99,11 +115,11 @@ fun DanmakuRegexFilterContent(
OutlinedTextField(
value = input,
onValueChange = { input = it; isError = false },
- placeholder = { Text("输入要屏蔽的弹幕关键词(正则)") },
- label = { Text("正则表达式") },
+ placeholder = { Text(placeholderText) },
+ label = { Text(regexExpressionText) },
supportingText = {
- if (isError) Text("正则表达式语法不正确。")
- else Text("例如:‘签’ 会屏蔽含文字‘签’的弹幕。")
+ if (isError) Text(invalidRegexText)
+ else Text(exampleText)
},
isError = isError,
singleLine = true,
@@ -116,7 +132,7 @@ fun DanmakuRegexFilterContent(
},
trailingIcon = {
IconButton(onClick = { add() }, enabled = !isBlank && valid) {
- Icon(Icons.Rounded.Add, contentDescription = "添加")
+ Icon(Icons.Rounded.Add, contentDescription = addText)
}
},
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
@@ -147,7 +163,7 @@ fun DanmakuRegexFilterContent(
onClick = { onDelete(item) },
modifier = Modifier.size(24.dp),
) {
- Icon(Icons.Rounded.Close, contentDescription = "删除", modifier = Modifier.size(16.dp))
+ Icon(Icons.Rounded.Close, contentDescription = deleteText, modifier = Modifier.size(16.dp))
}
},
colors = AssistChipDefaults.assistChipColors(
@@ -173,14 +189,14 @@ fun DanmakuRegexFilterSettings(
val layoutModifier = if (isPortrait) modifier.fillMaxWidth() else modifier
SideSheetLayout(
- title = { Text("正则弹幕过滤管理") },
+ title = { Text(stringResource(Lang.subject_episode_regex_filter_management)) },
onDismissRequest = onDismissRequest,
modifier = layoutModifier,
containerColor = backgroundColor,
closeButton = {
if (expanded) {
IconButton(onClick = onDismissRequest) {
- Icon(Icons.Rounded.Close, contentDescription = "关闭")
+ Icon(Icons.Rounded.Close, contentDescription = stringResource(Lang.subject_episode_close))
}
}
},
@@ -207,4 +223,3 @@ fun PreviewEditDanmakuRegexFilterSideSheet() {
)
}
}
-
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EpisodeSelectorSideSheet.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EpisodeSelectorSideSheet.kt
index 4da52c24a5..6284eb0a2b 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EpisodeSelectorSideSheet.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EpisodeSelectorSideSheet.kt
@@ -51,12 +51,17 @@ import me.him188.ani.app.ui.foundation.BackgroundScope
import me.him188.ani.app.ui.foundation.HasBackgroundScope
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.icons.PlayingIcon
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_close
+import me.him188.ani.app.ui.lang.subject_episode_now_playing
+import me.him188.ani.app.ui.lang.video_player_select_episode
import me.him188.ani.app.ui.subject.episode.EpisodePresentation
import me.him188.ani.app.ui.subject.episode.TAG_EPISODE_SELECTOR_SHEET
import me.him188.ani.app.ui.subject.episode.video.components.EpisodeVideoSideSheets
import me.him188.ani.app.ui.subject.episode.video.settings.SideSheetLayout
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -131,13 +136,17 @@ fun EpisodeVideoSideSheets.EpisodeSelectorSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val selectEpisodeText = stringResource(Lang.video_player_select_episode)
+ val closeText = stringResource(Lang.subject_episode_close)
+ val nowPlayingText = stringResource(Lang.subject_episode_now_playing)
+
SideSheetLayout(
onDismissRequest = onDismissRequest,
modifier = modifier.testTag(TAG_EPISODE_SELECTOR_SHEET),
- title = { Text(text = "选择剧集") },
+ title = { Text(text = selectEpisodeText) },
closeButton = {
IconButton(onClick = onDismissRequest) {
- Icon(Icons.Rounded.Close, contentDescription = "关闭")
+ Icon(Icons.Rounded.Close, contentDescription = closeText)
}
},
) {
@@ -172,7 +181,7 @@ fun EpisodeVideoSideSheets.EpisodeSelectorSheet(
},
trailingContent = {
if (selected) {
- PlayingIcon(contentDescription = "正在播放")
+ PlayingIcon(contentDescription = nowPlayingText)
}
},
colors =
@@ -250,9 +259,10 @@ fun PreviewEpisodeSelectorSideSheet() {
@Composable
fun PreviewPlayingIcon() {
ProvideCompositionLocalsForPreview {
+ val nowPlayingText = stringResource(Lang.subject_episode_now_playing)
Box(Modifier.size(64.dp), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.border(1.dp, color = Color.Magenta)) {
- PlayingIcon(contentDescription = "正在播放")
+ PlayingIcon(contentDescription = nowPlayingText)
}
}
}
diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EpisodeVideoMediaSelectorSideSheet.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EpisodeVideoMediaSelectorSideSheet.kt
index 90f9ba982a..22fbbe88e4 100644
--- a/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EpisodeVideoMediaSelectorSideSheet.kt
+++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/video/sidesheet/EpisodeVideoMediaSelectorSideSheet.kt
@@ -27,6 +27,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_close_selector
+import me.him188.ani.app.ui.lang.subject_episode_select_media_source
import me.him188.ani.app.ui.mediafetch.MediaSelectorState
import me.him188.ani.app.ui.mediafetch.MediaSelectorView
import me.him188.ani.app.ui.mediafetch.MediaSourceResultListPresentation
@@ -39,6 +42,7 @@ import me.him188.ani.app.ui.subject.episode.video.components.EpisodeVideoSideShe
import me.him188.ani.app.ui.subject.episode.video.settings.SideSheetLayout
import me.him188.ani.datasources.api.source.MediaFetchRequest
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
@Suppress("UnusedReceiverParameter")
@Composable
@@ -54,13 +58,16 @@ fun EpisodeVideoSideSheets.MediaSelectorSheet(
onRestartSource: (instanceId: String) -> Unit,
modifier: Modifier = Modifier,
) {
+ val selectMediaSourceText = stringResource(Lang.subject_episode_select_media_source)
+ val closeSelectorText = stringResource(Lang.subject_episode_close_selector)
+
SideSheetLayout(
- title = { Text(text = "选择数据源") },
+ title = { Text(text = selectMediaSourceText) },
onDismissRequest = onDismissRequest,
Modifier.testTag(TAG_MEDIA_SELECTOR_SHEET),
closeButton = {
IconButton(onClick = onDismissRequest) {
- Icon(Icons.Rounded.Close, contentDescription = "关闭")
+ Icon(Icons.Rounded.Close, contentDescription = closeSelectorText)
}
},
) {
diff --git a/app/shared/src/commonTest/kotlin/ui/subject/collection/components/AiringProgressTests.kt b/app/shared/src/commonTest/kotlin/ui/subject/collection/components/AiringProgressTests.kt
index b1f54070b4..a36f56069e 100644
--- a/app/shared/src/commonTest/kotlin/ui/subject/collection/components/AiringProgressTests.kt
+++ b/app/shared/src/commonTest/kotlin/ui/subject/collection/components/AiringProgressTests.kt
@@ -23,6 +23,7 @@ import me.him188.ani.app.tools.WeekFormatter
import me.him188.ani.app.ui.foundation.stateOf
import me.him188.ani.app.ui.subject.AiringLabelState
import me.him188.ani.app.ui.subject.SubjectProgressState
+import me.him188.ani.app.ui.subject.SubjectStatusStrings
import me.him188.ani.datasources.api.EpisodeSort
import me.him188.ani.datasources.api.PackedDate
import me.him188.ani.datasources.api.PackedDate.Companion.Invalid
@@ -38,14 +39,31 @@ import kotlin.time.Instant
@TestContainer
class AiringProgressTests {
private val today = Instant.parse("2024-08-23T12:00:00Z")
+ private val strings = SubjectStatusStrings(
+ continueWatchingFormat = "继续观看 %1\$s",
+ done = "已看完",
+ notOnAir = "还未开播",
+ startsOnFormat = "%1\$s开播",
+ startWatching = "开始观看",
+ updatesOnFormat = "%1\$s更新",
+ watchedFormat = "看过 %1\$s",
+ unknown = "未知",
+ upcoming = "未开播",
+ onAir = "连载中",
+ onAirToFormat = "连载至 %1\$s",
+ completed = "已完结",
+ totalEpisodesCompletedFormat = "全 %1\$s 话",
+ totalEpisodesScheduledFormat = "预定全 %1\$s 话",
+ )
private class Scope(
val airingLabelState: AiringLabelState,
val subjectProgressState: SubjectProgressState,
+ val strings: SubjectStatusStrings,
) {
- val airingLabel get() = airingLabelState.run { """$progressText · $totalEpisodesText""" }
+ val airingLabel get() = airingLabelState.run { """${progressText(strings)} · ${totalEpisodesText(strings)}""" }
val highlightProgress get() = airingLabelState.highlightProgress
- val buttonText get() = subjectProgressState.buttonText
+ val buttonText get() = subjectProgressState.buttonText(strings)
val buttonIsPrimary get() = subjectProgressState.buttonIsPrimary
}
@@ -85,6 +103,7 @@ class AiringProgressTests {
stateOf(subjectProgressInfo),
weekFormatter = WeekFormatter { today },
),
+ strings,
)
}
diff --git a/app/shared/src/desktopTest/kotlin/ui/onboarding/navigation/WizardNavHostTest.kt b/app/shared/src/desktopTest/kotlin/ui/onboarding/navigation/WizardNavHostTest.kt
index 0cbdc118a8..885e338e12 100644
--- a/app/shared/src/desktopTest/kotlin/ui/onboarding/navigation/WizardNavHostTest.kt
+++ b/app/shared/src/desktopTest/kotlin/ui/onboarding/navigation/WizardNavHostTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024-2025 OpenAni and contributors.
+ * Copyright (C) 2024-2026 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
@@ -21,8 +21,12 @@ import androidx.compose.ui.test.hasTextExactly
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
+import kotlinx.coroutines.runBlocking
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.framework.runAniComposeUiTest
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.onboarding_navigation_step_indicator
+import org.jetbrains.compose.resources.getString
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -32,6 +36,10 @@ private const val TAG_BUTTON_NEXT_STEP = "buttonNextStep"
private const val TAG_BUTTON_PREV_STEP = "buttonPrevStep"
private const val TAG_STEP_CONTENT_TEXT = "stepContentText"
+private fun expectedStepIndicatorText(currentStep: Int, totalStep: Int): String = runBlocking {
+ getString(Lang.onboarding_navigation_step_indicator, currentStep, totalStep)
+}
+
class WizardNavHostTest {
private val SemanticsNodeInteractionsProvider.indicatorText
get() = onAllNodesWithTag(TAG_INDICATOR_TEXT, useUnmergedTree = true)
@@ -99,7 +107,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(1, 3)),
+ hasTextExactly(expectedStepIndicatorText(1, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 1"))
stepContentText.assertTextEquals("this is my first step")
@@ -111,7 +119,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(1, 3)),
+ hasTextExactly(expectedStepIndicatorText(1, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 1"))
stepContentText.assertTextEquals("this is my first step")
@@ -123,7 +131,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(2, 3)),
+ hasTextExactly(expectedStepIndicatorText(2, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 2"))
stepContentText.assertTextEquals("this is my second step")
@@ -135,7 +143,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(3, 3)),
+ hasTextExactly(expectedStepIndicatorText(3, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 3"))
stepContentText.assertTextEquals("this is my third step")
@@ -147,7 +155,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(3, 3)),
+ hasTextExactly(expectedStepIndicatorText(3, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 3"))
stepContentText.assertTextEquals("this is my third step")
@@ -173,7 +181,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(1, 3)),
+ hasTextExactly(expectedStepIndicatorText(1, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 1"))
stepContentText.assertTextEquals("this is my first step")
@@ -185,7 +193,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(3, 3)),
+ hasTextExactly(expectedStepIndicatorText(3, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 3"))
stepContentText.assertTextEquals("this is my third step")
@@ -197,7 +205,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(3, 3)),
+ hasTextExactly(expectedStepIndicatorText(3, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 3"))
stepContentText.assertTextEquals("this is my third step")
@@ -211,7 +219,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(3, 3)),
+ hasTextExactly(expectedStepIndicatorText(3, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 3"))
stepContentText.assertTextEquals("this is my third step")
@@ -237,7 +245,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(1, 3)),
+ hasTextExactly(expectedStepIndicatorText(1, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 1"))
stepContentText.assertTextEquals("this is my first step")
@@ -249,7 +257,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(2, 3)),
+ hasTextExactly(expectedStepIndicatorText(2, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 2"))
stepContentText.assertTextEquals("this is my second step")
@@ -261,7 +269,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(2, 3)),
+ hasTextExactly(expectedStepIndicatorText(2, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 2"))
stepContentText.assertTextEquals("this is my second step")
@@ -275,7 +283,7 @@ class WizardNavHostTest {
runOnIdle {
indicatorText.assertAll(
- hasTextExactly(WizardDefaults.renderStepIndicatorText(2, 3)),
+ hasTextExactly(expectedStepIndicatorText(2, 3)),
)
indicatorTitle.assertAll(hasTextExactly("Step 2"))
stepContentText.assertTextEquals("this is my second step")
@@ -283,4 +291,4 @@ class WizardNavHostTest {
assertEquals(2, completedCount, "Expected trigger onCompleted once")
}
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/CacheManagementScreen.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/CacheManagementScreen.kt
index 72ec29012b..2b04d3e270 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/CacheManagementScreen.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/CacheManagementScreen.kt
@@ -110,9 +110,33 @@ import me.him188.ani.app.ui.foundation.session.SelfAvatar
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_episode_pause_download
+import me.him188.ani.app.ui.lang.cache_episode_resume_download
+import me.him188.ani.app.ui.lang.cache_management_delete_cache_confirmation
+import me.him188.ani.app.ui.lang.cache_management_delete_cache_title
+import me.him188.ani.app.ui.lang.cache_management_delete_selected
+import me.him188.ani.app.ui.lang.cache_management_downloading_count
+import me.him188.ani.app.ui.lang.cache_management_enter_selection_mode
+import me.him188.ani.app.ui.lang.cache_management_episode_label
+import me.him188.ani.app.ui.lang.cache_management_exit_selection
+import me.him188.ani.app.ui.lang.cache_management_finished_count
+import me.him188.ani.app.ui.lang.cache_management_invalid_cache_info
+import me.him188.ani.app.ui.lang.cache_management_more_actions
+import me.him188.ani.app.ui.lang.cache_management_more_info
+import me.him188.ani.app.ui.lang.cache_management_play
+import me.him188.ani.app.ui.lang.cache_management_select_all
+import me.him188.ani.app.ui.lang.cache_management_select_item_for_details
+import me.him188.ani.app.ui.lang.cache_management_selected_count
+import me.him188.ani.app.ui.lang.cache_management_streaming_not_supported
+import me.him188.ani.app.ui.lang.cache_subject_cancel
+import me.him188.ani.app.ui.lang.cache_subject_delete
+import me.him188.ani.app.ui.lang.cache_unknown
+import me.him188.ani.app.ui.lang.main_screen_page_cache_management
import me.him188.ani.app.ui.settings.rendering.P2p
import me.him188.ani.app.ui.user.SelfInfoUiState
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
/**
* 全局缓存管理页面状态
@@ -462,7 +486,7 @@ private fun CacheManagementLayout(
.padding(vertical = 48.dp),
contentAlignment = Alignment.Center,
) {
- Text("选择一个条目查看具体缓存")
+ Text(stringResource(Lang.cache_management_select_item_for_details))
}
}
} else {
@@ -510,10 +534,14 @@ private fun CacheManagementTopBar(
scrollBehavior: TopAppBarScrollBehavior?,
) {
if (selectionMode) {
+ val selectedCountText = stringResource(Lang.cache_management_selected_count, selectionCount)
+ val exitSelectionText = stringResource(Lang.cache_management_exit_selection)
+ val selectAllText = stringResource(Lang.cache_management_select_all)
+ val deleteSelectedText = stringResource(Lang.cache_management_delete_selected)
AniTopAppBar(
- title = { AniTopAppBarDefaults.Title("$selectionCount 个已选") },
+ title = { AniTopAppBarDefaults.Title(selectedCountText) },
navigationIcon = {
- IconButton(onClick = onExitSelection) { Icon(Icons.Rounded.Close, "退出选择") }
+ IconButton(onClick = onExitSelection) { Icon(Icons.Rounded.Close, exitSelectionText) }
},
actions = {
IconButton(
@@ -522,7 +550,7 @@ private fun CacheManagementTopBar(
) {
Icon(
if (allSelected) Icons.Default.Deselect else Icons.Default.SelectAll,
- "选择所有",
+ selectAllText,
)
}
},
@@ -531,7 +559,7 @@ private fun CacheManagementTopBar(
onClick = onDeleteSelected,
enabled = selectionCount > 0,
) {
- Icon(Icons.Rounded.Delete, "删除所选", tint = MaterialTheme.colorScheme.error)
+ Icon(Icons.Rounded.Delete, deleteSelectedText, tint = MaterialTheme.colorScheme.error)
}
},
colors = appBarColors,
@@ -540,14 +568,15 @@ private fun CacheManagementTopBar(
)
} else {
AniTopAppBar(
- title = { AniTopAppBarDefaults.Title("缓存管理") },
+ title = { AniTopAppBarDefaults.Title(stringResource(Lang.main_screen_page_cache_management)) },
navigationIcon = navigationIcon,
actions = {
+ val enterSelectionModeText = stringResource(Lang.cache_management_enter_selection_mode)
IconButton(
onClick = onEnterSelection,
enabled = hasEntries,
) {
- Icon(Icons.Default.Checklist, "进入选择模式")
+ Icon(Icons.Default.Checklist, enterSelectionModeText)
}
},
avatar = selfInfo?.let {
@@ -575,15 +604,15 @@ internal fun DeleteActionDialog(
AlertDialog(
onDismissRequest = onDismiss,
icon = { Icon(Icons.Rounded.Delete, null, tint = MaterialTheme.colorScheme.error) },
- title = { Text("删除缓存") },
- text = { Text("删除后不可恢复,确认删除吗?") },
+ title = { Text(stringResource(Lang.cache_management_delete_cache_title)) },
+ text = { Text(stringResource(Lang.cache_management_delete_cache_confirmation)) },
confirmButton = {
TextButton(
onClick = onConfirm,
- ) { Text("删除", color = MaterialTheme.colorScheme.error) }
+ ) { Text(stringResource(Lang.cache_subject_delete), color = MaterialTheme.colorScheme.error) }
},
dismissButton = {
- TextButton(onDismiss) { Text("取消") }
+ TextButton(onDismiss) { Text(stringResource(Lang.cache_subject_cancel)) }
},
)
}
@@ -617,6 +646,11 @@ private fun CacheSubjectListItem(
Modifier.weight(1f).animateContentSize(),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
+ val finishedCountText = stringResource(
+ Lang.cache_management_finished_count,
+ group.finishedCount,
+ group.entries.size,
+ )
Text(
group.subjectName,
style = MaterialTheme.typography.titleMedium,
@@ -628,13 +662,17 @@ private fun CacheSubjectListItem(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- "${group.finishedCount}/${group.entries.size} 已完成",
+ finishedCountText,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (group.downloadingCount > 0) {
+ val downloadingCountText = stringResource(
+ Lang.cache_management_downloading_count,
+ group.downloadingCount,
+ )
Text(
- "${group.downloadingCount} 个下载中",
+ downloadingCountText,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
@@ -720,7 +758,12 @@ private fun CacheListItem(
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
entry.engineKey?.let { key ->
- val (icon, desc) = renderEngineIcon(key)
+ val icon = renderEngineIcon(key)
+ val desc = when (key) {
+ MediaCacheEngineKey.Anitorrent -> "BT"
+ MediaCacheEngineKey.WebM3u -> "Web"
+ else -> stringResource(Lang.cache_unknown)
+ }
Icon(icon, desc, tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
@@ -732,7 +775,7 @@ private fun CacheListItem(
)
}
Text(
- "第${entry.sort}话 · ${entry.displayName}",
+ stringResource(Lang.cache_management_episode_label, entry.sort, entry.displayName),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -750,8 +793,9 @@ private fun CacheListItem(
onCheckedChange = { onToggleSelected() },
)
} else {
+ val moreActionsText = stringResource(Lang.cache_management_more_actions)
IconButton(onClick = { showMenu = true }) {
- Icon(Icons.Rounded.MoreVert, "更多操作")
+ Icon(Icons.Rounded.MoreVert, moreActionsText)
}
}
@@ -810,9 +854,9 @@ private fun CacheListItem(
}
private fun renderEngineIcon(key: MediaCacheEngineKey) = when (key) {
- MediaCacheEngineKey.Anitorrent -> Icons.Filled.P2p to "BT"
- MediaCacheEngineKey.WebM3u -> Icons.Filled.Language to "Web"
- else -> Icons.AutoMirrored.Rounded.HelpOutline to "未知"
+ MediaCacheEngineKey.Anitorrent -> Icons.Filled.P2p
+ MediaCacheEngineKey.WebM3u -> Icons.Filled.Language
+ else -> Icons.AutoMirrored.Rounded.HelpOutline
}
@Composable
@@ -829,6 +873,12 @@ internal fun CacheActionDropdown(
offset: DpOffset = DpOffset.Zero,
) {
val toaster = LocalToaster.current
+ val resumeDownloadText = stringResource(Lang.cache_episode_resume_download)
+ val pauseDownloadText = stringResource(Lang.cache_episode_pause_download)
+ val playText = stringResource(Lang.cache_management_play)
+ val invalidCacheInfoText = stringResource(Lang.cache_management_invalid_cache_info)
+ val streamingNotSupportedText = stringResource(Lang.cache_management_streaming_not_supported)
+ val moreInfoText = stringResource(Lang.cache_management_more_info)
DropdownMenu(
expanded = show,
onDismissRequest = onDismiss,
@@ -838,7 +888,7 @@ internal fun CacheActionDropdown(
if (!episode.isFinished) {
if (episode.isPaused) {
DropdownMenuItem(
- text = { Text("继续下载") },
+ text = { Text(resumeDownloadText) },
leadingIcon = { Icon(Icons.Rounded.Restore, null) },
onClick = {
onResume()
@@ -847,7 +897,7 @@ internal fun CacheActionDropdown(
)
} else if (!episode.isFailed) {
DropdownMenuItem(
- text = { Text("暂停下载") },
+ text = { Text(pauseDownloadText) },
leadingIcon = { Icon(Icons.Rounded.Pause, null) },
onClick = {
onPause()
@@ -858,7 +908,7 @@ internal fun CacheActionDropdown(
}
if (!episode.isFailed) {
DropdownMenuItem(
- text = { Text("播放") },
+ text = { Text(playText) },
leadingIcon = { Icon(Icons.Rounded.PlayArrow, null) },
onClick = {
when (episode.playability) {
@@ -868,11 +918,11 @@ internal fun CacheActionDropdown(
}
CacheEpisodeState.Playability.INVALID_SUBJECT_EPISODE_ID -> {
- toaster.toast("缓存信息无效,无法播放")
+ toaster.toast(invalidCacheInfoText)
}
CacheEpisodeState.Playability.STREAMING_NOT_SUPPORTED -> {
- toaster.toast("此资源不支持边下边播,请等待下载完成")
+ toaster.toast(streamingNotSupportedText)
}
}
},
@@ -880,7 +930,7 @@ internal fun CacheActionDropdown(
}
onViewDetail?.let {
DropdownMenuItem(
- text = { Text("更多信息") },
+ text = { Text(moreInfoText) },
leadingIcon = { Icon(Icons.Rounded.Info, null) },
onClick = {
it()
@@ -890,7 +940,7 @@ internal fun CacheActionDropdown(
}
DropdownMenuItem(
- text = { Text("删除", color = MaterialTheme.colorScheme.error) },
+ text = { Text(stringResource(Lang.cache_subject_delete), color = MaterialTheme.colorScheme.error) },
leadingIcon = { Icon(Icons.Rounded.Delete, null, tint = MaterialTheme.colorScheme.error) },
onClick = {
onDelete()
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheEpisodeItem.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheEpisodeItem.kt
index 42f2c7af33..4d9ce38f04 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheEpisodeItem.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheEpisodeItem.kt
@@ -67,9 +67,18 @@ import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
import me.him188.ani.app.ui.foundation.text.ProvideContentColor
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_episode_cover
+import me.him188.ani.app.ui.lang.cache_episode_download_completed
+import me.him188.ani.app.ui.lang.cache_episode_download_failed
+import me.him188.ani.app.ui.lang.cache_episode_downloading
+import me.him188.ani.app.ui.lang.cache_episode_manage_item
+import me.him188.ani.app.ui.lang.cache_episode_pause_download
+import me.him188.ani.app.ui.lang.cache_episode_resume_download
import me.him188.ani.datasources.api.topic.FileSize.Companion.Unspecified
import me.him188.ani.datasources.api.topic.FileSize.Companion.megaBytes
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
@Immutable
enum class CacheEpisodePaused {
@@ -93,6 +102,10 @@ fun CacheEpisodeItem(
var showConfirm by rememberSaveable { mutableStateOf(false) }
val listItemColors = ListItemDefaults.colors(containerColor = containerColor)
val scope = rememberUiMonoTasker()
+ val coverText = stringResource(Lang.cache_episode_cover)
+ val resumeDownloadText = stringResource(Lang.cache_episode_resume_download)
+ val pauseDownloadText = stringResource(Lang.cache_episode_pause_download)
+ val manageItemText = stringResource(Lang.cache_episode_manage_item)
if (showConfirm) {
DeleteActionDialog(
@@ -120,7 +133,7 @@ fun CacheEpisodeItem(
modifier.clickableAndMouseRightClick { showDropdown = true },
leadingContent = if (state.screenShots.isEmpty()) null else {
{
- AsyncImage(state.screenShots.first(), "封面")
+ AsyncImage(state.screenShots.first(), coverText)
}
},
supportingContent = {
@@ -212,11 +225,11 @@ fun CacheEpisodeItem(
if (!state.isFinished) {
if (state.isPaused) {
IconButton(onResume) {
- Icon(Icons.Rounded.Restore, "继续下载")
+ Icon(Icons.Rounded.Restore, resumeDownloadText)
}
} else if (!state.isFailed) {
IconButton(onPause) {
- Icon(Icons.Rounded.Pause, "暂停下载", Modifier.size(28.dp))
+ Icon(Icons.Rounded.Pause, pauseDownloadText, Modifier.size(28.dp))
}
}
}
@@ -225,7 +238,7 @@ fun CacheEpisodeItem(
// 总是展示的更多操作. 实际上点击整个 ListItem 都能展示 dropdown, 但留有这个按钮避免用户无法发现点击 list 能展开.
IconButton({ showDropdown = true }) {
- Icon(Icons.Rounded.MoreVert, "管理此项")
+ Icon(Icons.Rounded.MoreVert, manageItemText)
}
}
CacheActionDropdown(
@@ -249,12 +262,16 @@ internal fun DownloadStateIcon(
modifier: Modifier = Modifier
) {
when (state) {
- CacheEpisodePaused.COMPLETED -> Icon(Icons.Rounded.DownloadDone, "下载完成")
+ CacheEpisodePaused.COMPLETED -> Icon(
+ Icons.Rounded.DownloadDone,
+ stringResource(Lang.cache_episode_download_completed),
+ modifier,
+ )
CacheEpisodePaused.FAILED -> ProvideContentColor(MaterialTheme.colorScheme.error) {
- Icon(Icons.Rounded.FileDownloadOff, "下载失败")
+ Icon(Icons.Rounded.FileDownloadOff, stringResource(Lang.cache_episode_download_failed), modifier)
}
- else -> Icon(Icons.Rounded.Downloading, "下载中")
+ else -> Icon(Icons.Rounded.Downloading, stringResource(Lang.cache_episode_downloading), modifier)
}
}
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheFilterAndSortBar.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheFilterAndSortBar.kt
index 69e2980629..bd811a9d8f 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheFilterAndSortBar.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheFilterAndSortBar.kt
@@ -43,7 +43,27 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import me.him188.ani.app.domain.media.cache.engine.MediaCacheEngineKey
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_filter_cache_type
+import me.him188.ani.app.ui.lang.cache_filter_collection_doing
+import me.him188.ani.app.ui.lang.cache_filter_collection_done
+import me.him188.ani.app.ui.lang.cache_filter_collection_dropped
+import me.him188.ani.app.ui.lang.cache_filter_collection_not_collected
+import me.him188.ani.app.ui.lang.cache_filter_collection_on_hold
+import me.him188.ani.app.ui.lang.cache_filter_collection_state
+import me.him188.ani.app.ui.lang.cache_filter_collection_wish
+import me.him188.ani.app.ui.lang.cache_filter_download_status
+import me.him188.ani.app.ui.lang.cache_filter_sort
+import me.him188.ani.app.ui.lang.cache_filter_sort_episode_asc
+import me.him188.ani.app.ui.lang.cache_filter_sort_episode_desc
+import me.him188.ani.app.ui.lang.cache_filter_sort_newest
+import me.him188.ani.app.ui.lang.cache_filter_sort_oldest
+import me.him188.ani.app.ui.lang.cache_filter_sort_subject_asc
+import me.him188.ani.app.ui.lang.cache_filter_sort_subject_desc
+import me.him188.ani.app.ui.lang.cache_filter_status_downloading
+import me.him188.ani.app.ui.lang.cache_filter_status_finished
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
+import org.jetbrains.compose.resources.stringResource
@Stable
internal class CacheFilterAndSortState {
@@ -186,7 +206,7 @@ private fun CollectionFilterChip(
onChange: (UnifiedCollectionType?) -> Unit,
) {
FilterPill(
- label = "收藏状态",
+ label = stringResource(Lang.cache_filter_collection_state),
selectedLabel = selected?.let { renderCollectionType(it) },
isSelected = selected != null,
onClick = { isSelected ->
@@ -212,7 +232,7 @@ private fun EngineFilterChip(
onChange: (MediaCacheEngineKey?) -> Unit,
) {
FilterPill(
- label = "缓存类型",
+ label = stringResource(Lang.cache_filter_cache_type),
selectedLabel = selected?.let { renderEngineKey(it) },
isSelected = selected != null,
enabled = options.isNotEmpty(),
@@ -238,13 +258,8 @@ private fun StatusFilterChip(
onChange: (CacheStatusFilter?) -> Unit,
) {
FilterPill(
- label = "下载状态",
- selectedLabel = selected?.let {
- when (it) {
- CacheStatusFilter.Downloading -> "下载中"
- CacheStatusFilter.Finished -> "已完成"
- }
- },
+ label = stringResource(Lang.cache_filter_download_status),
+ selectedLabel = selected?.let { renderStatusFilter(it) },
isSelected = selected != null,
onClick = { isSelected ->
if (isSelected) onChange(null)
@@ -252,14 +267,7 @@ private fun StatusFilterChip(
) { onDismiss ->
CacheStatusFilter.entries.forEach { option ->
DropdownMenuItem(
- text = {
- Text(
- when (option) {
- CacheStatusFilter.Downloading -> "下载中"
- CacheStatusFilter.Finished -> "已完成"
- },
- )
- },
+ text = { Text(renderStatusFilter(option)) },
onClick = {
onChange(option)
onDismiss()
@@ -319,12 +327,12 @@ private fun SortMenuButton(
shape = CircleShape,
colors = IconButtonDefaults.iconButtonColors(),
) {
- Icon(Icons.AutoMirrored.Rounded.Sort, "排序")
+ Icon(Icons.AutoMirrored.Rounded.Sort, stringResource(Lang.cache_filter_sort))
}
DropdownMenu(expanded = showSortMenu, onDismissRequest = { showSortMenu = false }) {
CacheSortOption.entries.forEach { option ->
DropdownMenuItem(
- text = { Text(option.label) },
+ text = { Text(renderSortOption(option)) },
trailingIcon = {
if (option == sortOption) {
Icon(Icons.Rounded.Check, null)
@@ -345,23 +353,44 @@ enum class CacheStatusFilter {
Finished,
}
-internal enum class CacheSortOption(val label: String) {
- Newest("最新下载"),
- Oldest("最早下载"),
- SubjectAsc("条目名 AZ"),
- SubjectDesc("条目名 ZA"),
- EpisodeAsc("剧集升序"),
- EpisodeDesc("剧集降序"),
+internal enum class CacheSortOption {
+ Newest,
+ Oldest,
+ SubjectAsc,
+ SubjectDesc,
+ EpisodeAsc,
+ EpisodeDesc,
}
+@Composable
+private fun renderStatusFilter(type: CacheStatusFilter): String {
+ return when (type) {
+ CacheStatusFilter.Downloading -> stringResource(Lang.cache_filter_status_downloading)
+ CacheStatusFilter.Finished -> stringResource(Lang.cache_filter_status_finished)
+ }
+}
+
+@Composable
+private fun renderSortOption(option: CacheSortOption): String {
+ return when (option) {
+ CacheSortOption.Newest -> stringResource(Lang.cache_filter_sort_newest)
+ CacheSortOption.Oldest -> stringResource(Lang.cache_filter_sort_oldest)
+ CacheSortOption.SubjectAsc -> stringResource(Lang.cache_filter_sort_subject_asc)
+ CacheSortOption.SubjectDesc -> stringResource(Lang.cache_filter_sort_subject_desc)
+ CacheSortOption.EpisodeAsc -> stringResource(Lang.cache_filter_sort_episode_asc)
+ CacheSortOption.EpisodeDesc -> stringResource(Lang.cache_filter_sort_episode_desc)
+ }
+}
+
+@Composable
private fun renderCollectionType(type: UnifiedCollectionType): String {
return when (type) {
- UnifiedCollectionType.WISH -> "想看"
- UnifiedCollectionType.DOING -> "在看"
- UnifiedCollectionType.DONE -> "看过"
- UnifiedCollectionType.ON_HOLD -> "搁置"
- UnifiedCollectionType.DROPPED -> "抛弃"
- UnifiedCollectionType.NOT_COLLECTED -> "未收藏"
+ UnifiedCollectionType.WISH -> stringResource(Lang.cache_filter_collection_wish)
+ UnifiedCollectionType.DOING -> stringResource(Lang.cache_filter_collection_doing)
+ UnifiedCollectionType.DONE -> stringResource(Lang.cache_filter_collection_done)
+ UnifiedCollectionType.ON_HOLD -> stringResource(Lang.cache_filter_collection_on_hold)
+ UnifiedCollectionType.DROPPED -> stringResource(Lang.cache_filter_collection_dropped)
+ UnifiedCollectionType.NOT_COLLECTED -> stringResource(Lang.cache_filter_collection_not_collected)
}
}
@@ -369,4 +398,4 @@ private fun renderEngineKey(key: MediaCacheEngineKey): String = when (key) {
MediaCacheEngineKey.Anitorrent -> "BT"
MediaCacheEngineKey.WebM3u -> "Web"
else -> key.key
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheManagementOverallStats.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheManagementOverallStats.kt
index 4a2f93748a..814bf26674 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheManagementOverallStats.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/components/CacheManagementOverallStats.kt
@@ -32,7 +32,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import me.him188.ani.app.domain.media.cache.engine.MediaStats
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_total_download
+import me.him188.ani.app.ui.lang.cache_total_upload
import me.him188.ani.datasources.api.topic.FileSize
+import org.jetbrains.compose.resources.stringResource
@Composable
@@ -47,7 +51,7 @@ fun CacheManagementOverallStats(
Stat(
title = {
Icon(Icons.Rounded.Upload, null)
- Text("总上传", style = MaterialTheme.typography.titleMedium)
+ Text(stringResource(Lang.cache_total_upload), style = MaterialTheme.typography.titleMedium)
},
speedText = {
Text(renderSpeed(remember(stats) { derivedStateOf { stats().uploadSpeed } }.value))
@@ -60,7 +64,7 @@ fun CacheManagementOverallStats(
Stat(
title = {
Icon(Icons.Rounded.Download, null)
- Text("总下载", style = MaterialTheme.typography.titleMedium)
+ Text(stringResource(Lang.cache_total_download), style = MaterialTheme.typography.titleMedium)
},
speedText = {
Text(renderSpeed(remember(stats) { derivedStateOf { stats().downloadSpeed } }.value))
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/details/CacheGroupDetailsPage.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/details/CacheGroupDetailsPage.kt
index 63a7640a33..f4dac67ca4 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/details/CacheGroupDetailsPage.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/details/CacheGroupDetailsPage.kt
@@ -43,8 +43,11 @@ import me.him188.ani.app.ui.foundation.animation.AniAnimatedVisibility
import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.interaction.WindowDragArea
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_details_title
import me.him188.ani.datasources.api.source.MediaSourceInfo
import me.him188.ani.utils.logging.logger
+import org.jetbrains.compose.resources.stringResource
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -123,7 +126,7 @@ fun MediaCacheDetailsScreen(
topBar = {
WindowDragArea {
TopAppBar(
- title = { Text("详情") },
+ title = { Text(stringResource(Lang.cache_details_title)) },
navigationIcon = navigationIcon,
colors = AniThemeDefaults.topAppBarColors(),
windowInsets = windowInsets.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top),
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/details/MediaDetailsLazyGrid.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/details/MediaDetailsLazyGrid.kt
index 459a3726a8..85a0220a90 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/details/MediaDetailsLazyGrid.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/details/MediaDetailsLazyGrid.kt
@@ -53,7 +53,31 @@ import me.him188.ani.app.tools.formatDateTime
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.setClipEntryText
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_details_browse_file
+import me.him188.ani.app.ui.lang.cache_details_copied
+import me.him188.ani.app.ui.lang.cache_details_copy
+import me.him188.ani.app.ui.lang.cache_details_downloader_status
+import me.him188.ani.app.ui.lang.cache_details_episode_range
+import me.him188.ani.app.ui.lang.cache_details_external_subtitle
+import me.him188.ani.app.ui.lang.cache_details_file_size
+import me.him188.ani.app.ui.lang.cache_details_file_type
+import me.him188.ani.app.ui.lang.cache_details_local_cache_path
+import me.him188.ani.app.ui.lang.cache_details_open_file_failed
+import me.him188.ani.app.ui.lang.cache_details_open_link
+import me.him188.ani.app.ui.lang.cache_details_original_download_link
+import me.him188.ani.app.ui.lang.cache_details_original_link
+import me.him188.ani.app.ui.lang.cache_details_publish_time
+import me.him188.ani.app.ui.lang.cache_details_resolution
+import me.him188.ani.app.ui.lang.cache_details_source
+import me.him188.ani.app.ui.lang.cache_details_source_local
+import me.him188.ani.app.ui.lang.cache_details_source_online
+import me.him188.ani.app.ui.lang.cache_details_subtitle_group
+import me.him188.ani.app.ui.lang.cache_details_subtitle_language
+import me.him188.ani.app.ui.lang.cache_details_total_segments
+import me.him188.ani.app.ui.lang.cache_unknown
import me.him188.ani.app.ui.media.MediaDetailsRenderer
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.settings.rendering.MediaSourceIcon
import me.him188.ani.datasources.api.CachedMedia
import me.him188.ani.datasources.api.Media
@@ -69,6 +93,8 @@ import me.him188.ani.datasources.mikan.MikanCNMediaSource
import me.him188.ani.utils.io.absolutePath
import me.him188.ani.utils.io.inSystem
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.getString
+import org.jetbrains.compose.resources.stringResource
@Immutable
data class MediaDetails(
@@ -168,8 +194,14 @@ fun MediaDetailsLazyGrid(
val clipboard = LocalClipboard.current
val fileRevealer = LocalContext.current.getComponentAccessors().fileRevealer
val scope = rememberCoroutineScope()
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
val toaster = LocalToaster.current
+ val copiedText = stringResource(Lang.cache_details_copied)
+ val copyText = stringResource(Lang.cache_details_copy)
+ val openLinkText = stringResource(Lang.cache_details_open_link)
+ val browseFileText = stringResource(Lang.cache_details_browse_file)
+ val unknownText = stringResource(Lang.cache_unknown)
LazyVerticalGrid(
GridCells.Adaptive(minSize = 500.dp),
modifier,
@@ -179,11 +211,11 @@ fun MediaDetailsLazyGrid(
{
scope.launch {
clipboard.setClipEntryText(value())
- toaster.toast("已复制")
+ toaster.toast(copiedText)
}
},
) {
- Icon(Icons.Rounded.ContentCopy, contentDescription = "复制")
+ Icon(Icons.Rounded.ContentCopy, contentDescription = copyText)
}
}
val browseContent = @Composable { url: String ->
@@ -194,12 +226,12 @@ fun MediaDetailsLazyGrid(
} else {
scope.launch {
clipboard.setClipEntryText(url)
- toaster.toast("已复制")
+ toaster.toast(copiedText)
}
}
},
) {
- Icon(Icons.Rounded.ArrowOutward, contentDescription = "打开链接")
+ Icon(Icons.Rounded.ArrowOutward, contentDescription = openLinkText)
}
}
val browseFile = @Composable { url: Path ->
@@ -210,12 +242,17 @@ fun MediaDetailsLazyGrid(
{
scope.launch {
if (!fileRevealer.revealFile(url)) {
- toaster.toast("打开文件失败 ${url.inSystem.absolutePath}")
+ toaster.toast(
+ getString(
+ Lang.cache_details_open_file_failed,
+ url.inSystem.absolutePath,
+ ),
+ )
}
}
},
) {
- Icon(Icons.Rounded.FileOpen, contentDescription = "浏览文件")
+ Icon(Icons.Rounded.FileOpen, contentDescription = browseFileText)
}
}
}
@@ -236,14 +273,14 @@ fun MediaDetailsLazyGrid(
}
item {
ListItem(
- headlineContent = { Text("剧集范围") },
+ headlineContent = { Text(stringResource(Lang.cache_details_episode_range)) },
leadingContent = { Icon(Icons.Rounded.Layers, contentDescription = null) },
supportingContent = {
val range = details.episodeRange
SelectionContainer {
Text(
when {
- range == null -> "未知"
+ range == null -> unknownText
range.isSingleEpisode() -> range.knownSorts.firstOrNull().toString()
else -> range.toString()
},
@@ -255,15 +292,17 @@ fun MediaDetailsLazyGrid(
if (showSourceInfo) {
item {
ListItem(
- headlineContent = { Text("数据源") },
+ headlineContent = { Text(stringResource(Lang.cache_details_source)) },
leadingContent = { MediaSourceIcon(details.sourceInfo, Modifier.size(24.dp)) },
supportingContent = {
val kind = when (details.kind) {
- MediaSourceKind.WEB -> "在线"
+ MediaSourceKind.WEB -> stringResource(Lang.cache_details_source_online)
MediaSourceKind.BitTorrent -> "BT"
- MediaSourceKind.LocalCache -> "本地"
+ MediaSourceKind.LocalCache -> stringResource(Lang.cache_details_source_local)
+ }
+ SelectionContainer {
+ Text("[$kind] ${details.sourceInfo?.displayName ?: unknownText}")
}
- SelectionContainer { Text("[$kind] ${details.sourceInfo?.displayName ?: "未知"}") }
},
trailingContent = run {
val originalUrl by rememberUpdatedState(details.originalUrl)
@@ -282,7 +321,7 @@ fun MediaDetailsLazyGrid(
}
item {
ListItem(
- headlineContent = { Text("字幕组") },
+ headlineContent = { Text(stringResource(Lang.cache_details_subtitle_group)) },
leadingContent = { Icon(Icons.Rounded.Subtitles, contentDescription = null) },
supportingContent = { SelectionContainer { Text(details.properties.alliance) } },
trailingContent = { copyContent { details.properties.alliance } },
@@ -290,15 +329,16 @@ fun MediaDetailsLazyGrid(
}
item {
ListItem(
- headlineContent = { Text("字幕语言") },
+ headlineContent = { Text(stringResource(Lang.cache_details_subtitle_language)) },
leadingContent = { Icon(Icons.Rounded.Subtitles, contentDescription = null) },
supportingContent = {
SelectionContainer {
Text(
- remember(details) {
+ remember(details, mediaDetailsStrings) {
MediaDetailsRenderer.renderSubtitleLanguages(
details.properties.subtitleKind,
details.properties.subtitleLanguageIds,
+ mediaDetailsStrings,
)
},
)
@@ -308,26 +348,26 @@ fun MediaDetailsLazyGrid(
}
item {
ListItem(
- headlineContent = { Text("发布时间") },
+ headlineContent = { Text(stringResource(Lang.cache_details_publish_time)) },
leadingContent = { Icon(Icons.Rounded.Event, contentDescription = null) },
supportingContent = { SelectionContainer { Text(formatDateTime(details.publishedTimeMillis)) } },
)
}
item {
ListItem(
- headlineContent = { Text("分辨率") },
+ headlineContent = { Text(stringResource(Lang.cache_details_resolution)) },
leadingContent = { Icon(Icons.Outlined.Hd, contentDescription = null) },
supportingContent = { SelectionContainer { Text(details.properties.resolution) } },
)
}
item {
ListItem(
- headlineContent = { Text("文件大小") },
+ headlineContent = { Text(stringResource(Lang.cache_details_file_size)) },
leadingContent = { Icon(Icons.Rounded.Description, contentDescription = null) },
supportingContent = {
SelectionContainer {
if (details.fileSize == FileSize.Unspecified) {
- Text("未知")
+ Text(unknownText)
} else {
Text(details.fileSize.toString())
}
@@ -337,7 +377,7 @@ fun MediaDetailsLazyGrid(
}
item {
ListItem(
- headlineContent = { Text("原始链接") },
+ headlineContent = { Text(stringResource(Lang.cache_details_original_link)) },
leadingContent = placeholderLeadingContent,
supportingContent = {
SelectionContainer {
@@ -350,7 +390,7 @@ fun MediaDetailsLazyGrid(
if (details.fileType != null) {
item {
ListItem(
- headlineContent = { Text("文件类型") },
+ headlineContent = { Text(stringResource(Lang.cache_details_file_type)) },
leadingContent = placeholderLeadingContent,
supportingContent = {
SelectionContainer {
@@ -370,7 +410,7 @@ fun MediaDetailsLazyGrid(
if (details.contentDownloadUri != null) {
item {
ListItem(
- headlineContent = { Text("原始下载链接") },
+ headlineContent = { Text(stringResource(Lang.cache_details_original_download_link)) },
leadingContent = { Icon(Icons.Rounded.VideoFile, contentDescription = null) },
supportingContent = {
SelectionContainer {
@@ -384,7 +424,7 @@ fun MediaDetailsLazyGrid(
if (details.localCacheFilePath != null) {
item {
ListItem(
- headlineContent = { Text("本地缓存路径") },
+ headlineContent = { Text(stringResource(Lang.cache_details_local_cache_path)) },
leadingContent = { Icon(Icons.Rounded.VideoFile, contentDescription = null) },
supportingContent = {
SelectionContainer {
@@ -400,7 +440,7 @@ fun MediaDetailsLazyGrid(
if (details.totalSegments != null) {
item {
ListItem(
- headlineContent = { Text("总片段数") },
+ headlineContent = { Text(stringResource(Lang.cache_details_total_segments)) },
leadingContent = placeholderLeadingContent,
supportingContent = {
Text(details.totalSegments.toString(), maxLines = 1, overflow = TextOverflow.Ellipsis)
@@ -411,7 +451,7 @@ fun MediaDetailsLazyGrid(
if (details.downloaderStatus != null) {
item {
ListItem(
- headlineContent = { Text("下载器内部状态") },
+ headlineContent = { Text(stringResource(Lang.cache_details_downloader_status)) },
leadingContent = placeholderLeadingContent,
supportingContent = {
Text(details.downloaderStatus, maxLines = 1, overflow = TextOverflow.Ellipsis)
@@ -425,13 +465,11 @@ fun MediaDetailsLazyGrid(
headlineContent = {
SelectionContainer {
Text(
- remember(subtitle) {
- buildString {
- append("外挂字幕 ${index + 1}")
- subtitle.language?.let {
- append(": ")
- append(it)
- }
+ buildString {
+ append(stringResource(Lang.cache_details_external_subtitle, index + 1))
+ subtitle.language?.let {
+ append(": ")
+ append(it)
}
},
)
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/AutoCacheGroup.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/AutoCacheGroup.kt
index de9839f957..607e312603 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/AutoCacheGroup.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/AutoCacheGroup.kt
@@ -14,8 +14,11 @@ import androidx.compose.material.icons.automirrored.rounded.ViewList
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_subject_manage_all
import me.him188.ani.app.ui.settings.framework.components.RowButtonItem
import me.him188.ani.app.ui.settings.framework.components.SettingsScope
+import org.jetbrains.compose.resources.stringResource
@Composable
fun SettingsScope.AutoCacheGroup(
@@ -71,5 +74,5 @@ fun SettingsScope.AutoCacheGroup(
RowButtonItem(
onClick = onClickGlobalCacheManage,
icon = { Icon(Icons.AutoMirrored.Rounded.ViewList, null) },
- ) { Text("管理全部缓存") }
+ ) { Text(stringResource(Lang.cache_subject_manage_all)) }
}
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/CacheListGroup.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/CacheListGroup.kt
index 7e4a75aea7..ef7fe33857 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/CacheListGroup.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/CacheListGroup.kt
@@ -80,6 +80,13 @@ import me.him188.ani.app.ui.foundation.layout.desktopTitleBar
import me.him188.ani.app.ui.foundation.layout.desktopTitleBarPadding
import me.him188.ani.app.ui.foundation.theme.stronglyWeaken
import me.him188.ani.app.ui.foundation.widgets.ProgressIndicatorHeight
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_filter_collection_done
+import me.him188.ani.app.ui.lang.cache_filter_collection_dropped
+import me.him188.ani.app.ui.lang.cache_subject_cache
+import me.him188.ani.app.ui.lang.cache_subject_cancel
+import me.him188.ani.app.ui.lang.cache_subject_delete
+import me.him188.ani.app.ui.lang.cache_subject_episode_cache
import me.him188.ani.app.ui.mediafetch.MediaSelectorView
import me.him188.ani.app.ui.mediafetch.MediaSourceInfoProvider
import me.him188.ani.app.ui.mediafetch.MediaSourceResultListPresentation
@@ -91,6 +98,7 @@ import me.him188.ani.app.ui.settings.framework.components.TextItem
import me.him188.ani.datasources.api.EpisodeSort
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
import me.him188.ani.datasources.api.topic.isDoneOrDropped
+import org.jetbrains.compose.resources.stringResource
import org.koin.mp.KoinPlatform
@@ -221,7 +229,7 @@ fun SettingsScope.EpisodeCacheListGroup(
}
Group(
- title = { Text("单集缓存") },
+ title = { Text(stringResource(Lang.cache_subject_episode_cache)) },
modifier = modifier,
) {
state.episodes.fastForEachIndexed { i, episodeCacheState ->
@@ -287,7 +295,7 @@ private fun ItemDropdown(
onDismissRequest()
},
text = {
- Text("删除")
+ Text(stringResource(Lang.cache_subject_delete))
},
leadingIcon = {
Icon(
@@ -340,13 +348,13 @@ fun SettingsScope.EpisodeCacheItem(
when (episode.info.watchStatus) {
UnifiedCollectionType.DONE -> {
Label(Modifier.padding(start = 8.dp)) {
- Text("看过")
+ Text(stringResource(Lang.cache_filter_collection_done))
}
}
UnifiedCollectionType.DROPPED -> {
Label(Modifier.padding(start = 8.dp)) {
- Text("抛弃")
+ Text(stringResource(Lang.cache_filter_collection_dropped))
}
}
@@ -399,7 +407,7 @@ fun EpisodeCacheActionIcon(
showCancel = false
},
) {
- Icon(Icons.Rounded.Close, "取消")
+ Icon(Icons.Rounded.Close, stringResource(Lang.cache_subject_cancel))
}
} else {
if (hasActionRunning) {
@@ -457,7 +465,7 @@ fun EpisodeCacheActionIcon(
if (canCache) {
CompositionLocalProvider(LocalContentColor providesDefault MaterialTheme.colorScheme.primary) {
IconButton(onClick) {
- Icon(Icons.Rounded.Download, "缓存")
+ Icon(Icons.Rounded.Download, stringResource(Lang.cache_subject_cache))
}
}
}
diff --git a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/MediaCacheStorageSelector.kt b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/MediaCacheStorageSelector.kt
index 0d815d53d0..bbd519b522 100644
--- a/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/MediaCacheStorageSelector.kt
+++ b/app/shared/ui-cache/src/commonMain/kotlin/ui/cache/subject/MediaCacheStorageSelector.kt
@@ -25,6 +25,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import me.him188.ani.app.domain.media.cache.storage.MediaCacheStorage
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_subject_cancel
+import me.him188.ani.app.ui.lang.cache_subject_select_storage
+import org.jetbrains.compose.resources.stringResource
@Composable
fun SelectMediaStorageDialog(
@@ -35,11 +39,11 @@ fun SelectMediaStorageDialog(
) {
AlertDialog(
onDismissRequest = onDismissRequest,
- title = { Text("选择储存位置") },
+ title = { Text(stringResource(Lang.cache_subject_select_storage)) },
icon = { Icon(Icons.Rounded.Save, null) },
confirmButton = {
TextButton(onDismissRequest) {
- Text("取消")
+ Text(stringResource(Lang.cache_subject_cancel))
}
},
text = {
diff --git a/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/CommentDefaults.kt b/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/CommentDefaults.kt
index 56eff64915..f87db9784e 100644
--- a/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/CommentDefaults.kt
+++ b/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/CommentDefaults.kt
@@ -49,6 +49,8 @@ import me.him188.ani.app.ui.foundation.avatar.AvatarImage
import me.him188.ani.app.ui.foundation.theme.stronglyWeaken
import me.him188.ani.app.ui.lang.Lang
import me.him188.ani.app.ui.lang.comment_empty_title
+import me.him188.ani.app.ui.lang.comment_reply
+import me.him188.ani.app.ui.lang.comment_view_more_replies
import me.him188.ani.app.ui.richtext.RichText
import me.him188.ani.app.ui.richtext.RichTextDefaults
import me.him188.ani.app.ui.richtext.UIRichElement
@@ -179,6 +181,7 @@ object CommentDefaults {
onClickBlock: () -> Unit,
onClickReport: () -> Unit
) {
+ val replyText = stringResource(Lang.comment_reply)
val size = EditCommentDefaults.ActionButtonSize.dp
val iconSize = 20.dp
Row(modifier = modifier) {
@@ -188,7 +191,7 @@ object CommentDefaults {
if (showReply) {
EditCommentDefaults.ActionButton(
imageVector = Icons.Outlined.ModeComment,
- contentDescription = "回复评论",
+ contentDescription = replyText,
onClick = onClickReply,
iconSize = iconSize,
)
@@ -240,7 +243,7 @@ object CommentDefaults {
}
if (hiddenReplyCount > 0) {
Text(
- text = "查看更多 $hiddenReplyCount 条回复>",
+ text = stringResource(Lang.comment_view_more_replies, hiddenReplyCount),
color = primaryColor,
modifier = Modifier
.fillMaxWidth()
diff --git a/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/EditComment.kt b/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/EditComment.kt
index 726d920b45..026bbee6fd 100644
--- a/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/EditComment.kt
+++ b/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/EditComment.kt
@@ -53,6 +53,11 @@ import me.him188.ani.app.ui.foundation.animation.AniAnimatedVisibility
import me.him188.ani.app.ui.foundation.ifThen
import me.him188.ani.app.ui.foundation.interaction.isImeVisible
import me.him188.ani.app.ui.foundation.text.ProvideContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.comment_ani_only_notice
+import me.him188.ani.app.ui.lang.comment_send_failed_network
+import me.him188.ani.app.ui.lang.comment_send_failed_unknown
+import org.jetbrains.compose.resources.stringResource
/**
* 评论编辑.
@@ -197,12 +202,14 @@ fun EditComment(
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth()
) {
+ val sendErrorText = when (val sendResult = state.sendResult) {
+ is CommentSendResult.Error -> renderCommentSendError(sendResult)
+ else -> ""
+ }
Text(
- text = (state.sendResult as? CommentSendResult.Error)
- ?.let { renderCommentSendError(it) }
- ?: "",
+ text = sendErrorText,
color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.bodyMedium
+ style = MaterialTheme.typography.bodyMedium,
)
}
}
@@ -213,8 +220,8 @@ fun EditComment(
@Composable
private fun renderCommentSendError(result: CommentSendResult.Error): String {
return when (result) {
- CommentSendResult.NetworkError -> "发送失败:网络错误"
- is CommentSendResult.UnknownError -> "发送失败,请附带日志反馈此问题\n${result.message}"
+ CommentSendResult.NetworkError -> stringResource(Lang.comment_send_failed_network)
+ is CommentSendResult.UnknownError -> stringResource(Lang.comment_send_failed_unknown, result.message)
}
}
@@ -267,7 +274,7 @@ fun EditCommentScaffold(
Row(Modifier.padding(horizontal = 8.dp)) {
Text(
- "评论将发送到 Ani,Bangumi 评论为只读",
+ stringResource(Lang.comment_ani_only_notice),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.outline,
)
diff --git a/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/EditCommentDefaults.kt b/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/EditCommentDefaults.kt
index 28c3518326..5ad4d12f77 100644
--- a/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/EditCommentDefaults.kt
+++ b/app/shared/ui-comment/src/commonMain/kotlin/ui/comment/EditCommentDefaults.kt
@@ -73,8 +73,24 @@ import me.him188.ani.app.ui.external.placeholder.placeholder
import me.him188.ani.app.ui.foundation.IconButton
import me.him188.ani.app.ui.foundation.LocalIsPreviewing
import me.him188.ani.app.ui.foundation.theme.looming
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.comment_add_emoji
+import me.him188.ani.app.ui.lang.comment_bold
+import me.him188.ani.app.ui.lang.comment_edit
+import me.him188.ani.app.ui.lang.comment_image
+import me.him188.ani.app.ui.lang.comment_italic
+import me.him188.ani.app.ui.lang.comment_link
+import me.him188.ani.app.ui.lang.comment_mask
+import me.him188.ani.app.ui.lang.comment_more_editor_actions
+import me.him188.ani.app.ui.lang.comment_preview
+import me.him188.ani.app.ui.lang.comment_rendering
+import me.him188.ani.app.ui.lang.comment_send
+import me.him188.ani.app.ui.lang.comment_send_comment
+import me.him188.ani.app.ui.lang.comment_strikethrough
+import me.him188.ani.app.ui.lang.comment_underline
import me.him188.ani.app.ui.richtext.RichText
import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.stringResource
object EditCommentDefaults {
@Suppress("ConstPropertyName")
@@ -99,7 +115,7 @@ object EditCommentDefaults {
@Composable
fun CommentTextFieldPlaceholder(modifier: Modifier = Modifier) {
- Text(text = "发送评论", modifier, softWrap = false)
+ Text(text = stringResource(Lang.comment_send_comment), modifier, softWrap = false)
}
@Composable
@@ -154,7 +170,7 @@ object EditCommentDefaults {
if (content == null) {
item {
Text(
- text = "渲染中...",
+ text = stringResource(Lang.comment_rendering),
modifier = Modifier.padding(contentPadding),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
)
@@ -226,6 +242,18 @@ object EditCommentDefaults {
val expandableActionWidth by animateDpAsState(if (actionRowExpanded) size else 0.dp)
val actionEnabled by derivedStateOf { !sending && !previewing }
+ val addEmojiText = stringResource(Lang.comment_add_emoji)
+ val boldText = stringResource(Lang.comment_bold)
+ val italicText = stringResource(Lang.comment_italic)
+ val underlineText = stringResource(Lang.comment_underline)
+ val strikethroughText = stringResource(Lang.comment_strikethrough)
+ val maskText = stringResource(Lang.comment_mask)
+ val imageText = stringResource(Lang.comment_image)
+ val linkText = stringResource(Lang.comment_link)
+ val moreEditorActionsText = stringResource(Lang.comment_more_editor_actions)
+ val editText = stringResource(Lang.comment_edit)
+ val previewText = stringResource(Lang.comment_preview)
+ val sendText = stringResource(Lang.comment_send)
// Custom FlowRow which supports a right-aligned element.
Layout(
@@ -304,7 +332,7 @@ object EditCommentDefaults {
content = {
ActionButton(
imageVector = Icons.Outlined.SentimentSatisfied,
- contentDescription = "添加表情",
+ contentDescription = addEmojiText,
onClick = onClickEmoji,
modifier = Modifier.size(size),
enabled = actionEnabled,
@@ -312,28 +340,28 @@ object EditCommentDefaults {
if (actionRowExpanded) {
ActionButton(
imageVector = Icons.Outlined.FormatBold,
- contentDescription = "加粗",
+ contentDescription = boldText,
onClick = onClickBold,
modifier = Modifier.size(height = size, width = expandableActionWidth),
enabled = actionEnabled,
)
ActionButton(
imageVector = Icons.Outlined.FormatItalic,
- contentDescription = "斜体",
+ contentDescription = italicText,
onClick = onClickItalic,
modifier = Modifier.size(height = size, width = expandableActionWidth),
enabled = actionEnabled,
)
ActionButton(
imageVector = Icons.Outlined.FormatUnderlined,
- contentDescription = "下划线",
+ contentDescription = underlineText,
onClick = onClickUnderlined,
modifier = Modifier.size(height = size, width = expandableActionWidth),
enabled = actionEnabled,
)
ActionButton(
imageVector = Icons.Outlined.FormatStrikethrough,
- contentDescription = "删除线",
+ contentDescription = strikethroughText,
onClick = onClickStrikethrough,
modifier = Modifier.size(height = size, width = expandableActionWidth),
enabled = actionEnabled,
@@ -341,14 +369,14 @@ object EditCommentDefaults {
}
ActionButton(
imageVector = Icons.Outlined.VisibilityOff,
- contentDescription = "遮罩",
+ contentDescription = maskText,
onClick = onClickMask,
modifier = Modifier.size(size),
enabled = actionEnabled,
)
ActionButton(
imageVector = Icons.Outlined.Image,
- contentDescription = "图片",
+ contentDescription = imageText,
onClick = onClickImage,
modifier = Modifier.size(size),
enabled = actionEnabled,
@@ -356,7 +384,7 @@ object EditCommentDefaults {
if (actionRowExpanded) {
ActionButton(
imageVector = Icons.Outlined.Link,
- contentDescription = "链接",
+ contentDescription = linkText,
onClick = onClickUrl,
modifier = Modifier.size(height = size, width = expandableActionWidth),
enabled = actionEnabled,
@@ -365,7 +393,7 @@ object EditCommentDefaults {
// 最后一个按钮不要有 ripple effect,因为有动画,看起来比较奇怪
ActionButton(
imageVector = Icons.Outlined.MoreHoriz,
- contentDescription = "更多评论编辑功能",
+ contentDescription = moreEditorActionsText,
enabled = true,
onClick = { actionRowExpanded = true },
modifier = Modifier.size(height = size, width = size - expandableActionWidth),
@@ -377,7 +405,7 @@ object EditCommentDefaults {
onClick = onPreview,
modifier = Modifier.padding(end = 4.dp),
) {
- Text(text = if (previewing) "编辑" else "预览")
+ Text(text = if (previewing) editText else previewText)
}
OutlinedButton(
onClick = onSend,
@@ -390,7 +418,7 @@ object EditCommentDefaults {
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
- Text(text = "发送", textAlign = TextAlign.Center)
+ Text(text = sendText, textAlign = TextAlign.Center)
Icon(
imageVector = Icons.AutoMirrored.Rounded.Send,
modifier = Modifier.size(24.dp),
@@ -442,4 +470,4 @@ object EditCommentDefaults {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/EditRatingDialog.kt b/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/EditRatingDialog.kt
index 248e5b4134..277cfe7db6 100644
--- a/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/EditRatingDialog.kt
+++ b/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/EditRatingDialog.kt
@@ -57,6 +57,29 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.icons.EditSquare
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.rating_comment_hint
+import me.him188.ani.app.ui.lang.rating_comment_label
+import me.him188.ani.app.ui.lang.rating_comment_optional
+import me.him188.ani.app.ui.lang.rating_discard
+import me.him188.ani.app.ui.lang.rating_discard_edit_message
+import me.him188.ani.app.ui.lang.rating_discard_edit_title
+import me.him188.ani.app.ui.lang.rating_edit_title
+import me.him188.ani.app.ui.lang.rating_private_only
+import me.him188.ani.app.ui.lang.rating_score_class_average
+import me.him188.ani.app.ui.lang.rating_score_class_bad
+import me.him188.ani.app.ui.lang.rating_score_class_highly_recommended
+import me.him188.ani.app.ui.lang.rating_score_class_legendary_caution
+import me.him188.ani.app.ui.lang.rating_score_class_masterpiece
+import me.him188.ani.app.ui.lang.rating_score_class_okay
+import me.him188.ani.app.ui.lang.rating_score_class_poor
+import me.him188.ani.app.ui.lang.rating_score_class_recommended
+import me.him188.ani.app.ui.lang.rating_score_class_terrible_caution
+import me.him188.ani.app.ui.lang.rating_score_class_very_bad
+import me.him188.ani.app.ui.lang.settings_danmaku_confirm
+import me.him188.ani.app.ui.lang.settings_media_source_continue_editing
+import me.him188.ani.app.ui.lang.settings_mediasource_cancel
+import org.jetbrains.compose.resources.stringResource
import kotlin.math.max
@Stable
@@ -91,12 +114,19 @@ fun RatingEditorDialog(
modifier: Modifier = Modifier,
isLoading: Boolean = false,
) {
+ val discardEditTitle = stringResource(Lang.rating_discard_edit_title)
+ val discardEditMessage = stringResource(Lang.rating_discard_edit_message)
+ val discardText = stringResource(Lang.rating_discard)
+ val continueEditingText = stringResource(Lang.settings_media_source_continue_editing)
+ val editRatingText = stringResource(Lang.rating_edit_title)
+ val confirmText = stringResource(Lang.settings_danmaku_confirm)
+ val cancelText = stringResource(Lang.settings_mediasource_cancel)
var showConfirmCancelDialog by remember { mutableStateOf(false) }
if (showConfirmCancelDialog) {
AlertDialog(
onDismissRequest = { showConfirmCancelDialog = false },
- title = { Text("舍弃编辑") },
- text = { Text("评价尚未保存,确定要舍弃吗?") },
+ title = { Text(discardEditTitle) },
+ text = { Text(discardEditMessage) },
confirmButton = {
TextButton(
onClick = {
@@ -107,14 +137,14 @@ fun RatingEditorDialog(
contentColor = MaterialTheme.colorScheme.error,
),
) {
- Text("舍弃")
+ Text(discardText)
}
},
dismissButton = {
TextButton(
onClick = { showConfirmCancelDialog = false },
) {
- Text("继续编辑")
+ Text(continueEditingText)
}
},
)
@@ -124,7 +154,7 @@ fun RatingEditorDialog(
AlertDialog(
onDismissRequest = onDismissRequest,
icon = { Icon(Icons.Rounded.EditSquare, null) },
- title = { Text("修改评分") },
+ title = { Text(editRatingText) },
text = {
RatingEditor(
state.score, { state.score = it },
@@ -144,7 +174,7 @@ fun RatingEditorDialog(
onRate(RateRequest(state.score, state.comment, state.isPrivate))
},
) {
- Text("确定")
+ Text(confirmText)
}
}
},
@@ -158,7 +188,7 @@ fun RatingEditorDialog(
}
},
) {
- Text("取消")
+ Text(cancelText)
}
},
properties = DialogProperties(
@@ -184,6 +214,11 @@ fun RatingEditor(
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
+ val scoreLabels = rememberRatingScoreLabels()
+ val commentLabelText = stringResource(Lang.rating_comment_label)
+ val commentHintText = stringResource(Lang.rating_comment_hint)
+ val commentOptionalText = stringResource(Lang.rating_comment_optional)
+ val privateOnlyText = stringResource(Lang.rating_private_only)
Column(modifier) {
Column(
Modifier.align(Alignment.CenterHorizontally),
@@ -205,7 +240,7 @@ fun RatingEditor(
color = scoreColor(score.toFloat()),
)
RatingScoreText(
- remember(score) { renderScoreClass(score.toFloat()) },
+ remember(score, scoreLabels) { renderScoreClass(score.toFloat(), scoreLabels) },
style = MaterialTheme.typography.bodyLarge,
color = scoreColor(score.toFloat()),
)
@@ -216,6 +251,7 @@ fun RatingEditor(
TenRatingStars(
score,
onScoreChange = onScoreChange,
+ scoreLabels = scoreLabels,
enabled = enabled,
)
}
@@ -236,13 +272,13 @@ fun RatingEditor(
shape = MaterialTheme.shapes.medium,
label = {
if (isFocused || comment.isNotEmpty()) {
- Text("评价")
+ Text(commentLabelText)
} else {
- Text("说点什么...")
+ Text(commentHintText)
}
},
interactionSource = interactionSource,
- placeholder = { Text("可留空") },
+ placeholder = { Text(commentOptionalText) },
readOnly = !enabled,
)
}
@@ -261,15 +297,16 @@ fun RatingEditor(
onCheckedChange = onIsPrivateChange,
enabled = enabled,
)
- Text("仅自己可见")
+ Text(privateOnlyText)
}
}
}
@Composable
-fun TenRatingStars(
+private fun TenRatingStars(
score: Int, // range 1..10
onScoreChange: (Int) -> Unit,
+ scoreLabels: RatingScoreLabels,
color: Color = MaterialTheme.colorScheme.primary,
modifier: Modifier = Modifier,
enabled: Boolean = true,
@@ -283,7 +320,7 @@ fun TenRatingStars(
val icon = @Composable { index: Int ->
Icon(
if (score >= index) Icons.Rounded.Star else Icons.Rounded.StarOutline,
- contentDescription = renderScoreClass(index.toFloat()),
+ contentDescription = renderScoreClass(index.toFloat(), scoreLabels),
Modifier
.clip(CircleShape)
.clickable(
@@ -310,19 +347,46 @@ fun TenRatingStars(
}
}
+private data class RatingScoreLabels(
+ val terribleCaution: String,
+ val veryBad: String,
+ val bad: String,
+ val poor: String,
+ val average: String,
+ val okay: String,
+ val recommended: String,
+ val highlyRecommended: String,
+ val masterpiece: String,
+ val legendaryCaution: String,
+)
+
+@Composable
+private fun rememberRatingScoreLabels(): RatingScoreLabels = RatingScoreLabels(
+ terribleCaution = stringResource(Lang.rating_score_class_terrible_caution),
+ veryBad = stringResource(Lang.rating_score_class_very_bad),
+ bad = stringResource(Lang.rating_score_class_bad),
+ poor = stringResource(Lang.rating_score_class_poor),
+ average = stringResource(Lang.rating_score_class_average),
+ okay = stringResource(Lang.rating_score_class_okay),
+ recommended = stringResource(Lang.rating_score_class_recommended),
+ highlyRecommended = stringResource(Lang.rating_score_class_highly_recommended),
+ masterpiece = stringResource(Lang.rating_score_class_masterpiece),
+ legendaryCaution = stringResource(Lang.rating_score_class_legendary_caution),
+)
+
@Stable
-fun renderScoreClass(score: Float): String {
+private fun renderScoreClass(score: Float, labels: RatingScoreLabels): String {
return when (score) {
- in 0f..1f -> "不忍直视(请谨慎评价)"
- in 1f..2f -> "很差"
- in 2f..3f -> "差"
- in 3f..4f -> "较差"
- in 4f..5f -> "不过不失"
- in 5f..6f -> "还行"
- in 6f..7f -> "推荐"
- in 7f..8f -> "力荐"
- in 8f..9f -> "神作"
- in 9f..10f -> "超神作(请谨慎评价)"
+ in 0f..1f -> labels.terribleCaution
+ in 1f..2f -> labels.veryBad
+ in 2f..3f -> labels.bad
+ in 3f..4f -> labels.poor
+ in 4f..5f -> labels.average
+ in 5f..6f -> labels.okay
+ in 6f..7f -> labels.recommended
+ in 7f..8f -> labels.highlyRecommended
+ in 8f..9f -> labels.masterpiece
+ in 9f..10f -> labels.legendaryCaution
else -> ""
}
}
diff --git a/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/EditableRating.kt b/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/EditableRating.kt
index 3d4e0dbc8d..6d58ac8dbe 100644
--- a/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/EditableRating.kt
+++ b/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/EditableRating.kt
@@ -29,11 +29,15 @@ import me.him188.ani.app.data.models.subject.SubjectInfo
import me.him188.ani.app.data.models.subject.TestSelfRatingInfo
import me.him188.ani.app.data.models.subject.TestSubjectInfo
import me.him188.ani.app.tools.MonoTasker
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.rating_requires_collection
+import me.him188.ani.app.ui.lang.settings_mediasource_close
import me.him188.ani.utils.analytics.Analytics
import me.him188.ani.utils.analytics.AnalyticsEvent.Companion.RatingEnter
import me.him188.ani.utils.analytics.AnalyticsEvent.Companion.RatingSubmit
import me.him188.ani.utils.analytics.recordEvent
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
@Stable
@@ -108,10 +112,10 @@ fun EditableRating(
if (state.showRatingRequiresCollectionDialog) {
AlertDialog(
{ state.dismissRatingRequiresCollectionDialog() },
- text = { Text("请先收藏再评分") },
+ text = { Text(stringResource(Lang.rating_requires_collection)) },
confirmButton = {
TextButton({ state.dismissRatingRequiresCollectionDialog() }) {
- Text("关闭")
+ Text(stringResource(Lang.settings_mediasource_close))
}
},
)
diff --git a/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/Rating.kt b/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/Rating.kt
index e2ebc5b34e..df1cc5f95e 100644
--- a/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/Rating.kt
+++ b/app/shared/ui-comment/src/commonMain/kotlin/ui/rating/Rating.kt
@@ -37,7 +37,11 @@ import androidx.compose.ui.unit.dp
import me.him188.ani.app.data.models.subject.RatingInfo
import me.him188.ani.app.data.models.subject.TestRatingInfo
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.rating_self_score
+import me.him188.ani.app.ui.lang.rating_summary
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
/**
* 展示自己的评分和评分信息
@@ -55,7 +59,7 @@ fun Rating(
if (selfRatingScore != 0) {
Row(Modifier.padding(horizontal = 2.dp).align(Alignment.End)) {
Text(
- remember(selfRatingScore) { "你的评分: $selfRatingScore" },
+ stringResource(Lang.rating_self_score, selfRatingScore),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -76,7 +80,7 @@ fun Rating(
Column(Modifier.padding(start = 8.dp), horizontalAlignment = Alignment.End) {
FiveRatingStars(score = rating.scoreFloat.toInt(), color = LocalContentColor.current)
Text(
- "${rating.total} 人评丨#${rating.rank}",
+ stringResource(Lang.rating_summary, rating.total, rating.rank),
Modifier.padding(end = 2.dp),
style = MaterialTheme.typography.labelMedium,
maxLines = 1,
diff --git a/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/AdaptivePlayerScreenLayout.kt b/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/AdaptivePlayerScreenLayout.kt
index 012c9e2194..4cac522ae5 100644
--- a/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/AdaptivePlayerScreenLayout.kt
+++ b/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/AdaptivePlayerScreenLayout.kt
@@ -77,9 +77,14 @@ import me.him188.ani.app.ui.foundation.layout.isHeightAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
import me.him188.ani.app.ui.foundation.pagerTabIndicatorOffset
import me.him188.ani.app.ui.foundation.theme.AniTheme
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.episode_comments
+import me.him188.ani.app.ui.lang.episode_comments_with_count
+import me.him188.ani.app.ui.lang.subject_details_tab_details
import me.him188.ani.app.ui.search.rememberTestLazyPagingItems
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.isMobile
+import org.jetbrains.compose.resources.stringResource
@Stable
class AdaptivePlayerScreenScaffoldState(
@@ -347,6 +352,7 @@ private fun TabRow(
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.surface,
) {
+ val detailsText = stringResource(Lang.subject_details_tab_details)
TabRow(
selectedTabIndex = pagerState.currentPage,
modifier,
@@ -364,7 +370,7 @@ private fun TabRow(
selected = pagerState.currentPage == 0,
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
modifier = Modifier.height(44.dp),
- text = { Text("详情", softWrap = false) },
+ text = { Text(detailsText, softWrap = false) },
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
)
@@ -373,11 +379,11 @@ private fun TabRow(
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
modifier = Modifier.height(44.dp),
text = {
- val text by remember(commentCount) {
- derivedStateOf {
- val count = commentCount()
- if (count == null) "评论" else "评论 $count"
- }
+ val count = commentCount()
+ val text = if (count == null) {
+ stringResource(Lang.episode_comments)
+ } else {
+ stringResource(Lang.episode_comments_with_count, count)
}
Text(text, softWrap = false)
},
diff --git a/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/PlayingEpisodeSummary.kt b/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/PlayingEpisodeSummary.kt
index 55006441a6..dc65871177 100644
--- a/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/PlayingEpisodeSummary.kt
+++ b/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/PlayingEpisodeSummary.kt
@@ -48,11 +48,15 @@ import me.him188.ani.app.data.models.subject.SelfRatingInfo
import me.him188.ani.app.data.models.subject.TestRatingInfo
import me.him188.ani.app.data.models.subject.TestSelfRatingInfo
import me.him188.ani.app.ui.foundation.AsyncImage
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.episode_summary_download
+import me.him188.ani.app.ui.lang.episode_summary_share
import me.him188.ani.app.ui.subject.collection.components.EditableSubjectCollectionTypeButton
import me.him188.ani.app.ui.subject.collection.components.EditableSubjectCollectionTypeState
import me.him188.ani.datasources.api.EpisodeSort
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
@Immutable
data class PlayingEpisodeSummary(
@@ -78,6 +82,8 @@ fun PlayingEpisodeSummaryRow(
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.surfaceContainerLowest,
) {
+ val shareText = stringResource(Lang.episode_summary_share)
+ val downloadText = stringResource(Lang.episode_summary_download)
Surface(color = containerColor) {
Column(modifier) {
if (expanded) {
@@ -140,8 +146,8 @@ fun PlayingEpisodeSummaryRow(
horizontalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.End),
verticalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.CenterVertically),
) {
- TonalButtonWithIcon("分享", Icons.Rounded.Share, onClickShare)
- TonalButtonWithIcon("下载", Icons.Rounded.Download, onClickDownload)
+ TonalButtonWithIcon(shareText, Icons.Rounded.Share, onClickShare)
+ TonalButtonWithIcon(downloadText, Icons.Rounded.Download, onClickDownload)
}
}
}
@@ -162,8 +168,8 @@ fun PlayingEpisodeSummaryRow(
horizontalArrangement = Arrangement.spacedBy(0.dp, alignment = Alignment.Start),
verticalArrangement = Arrangement.spacedBy(0.dp, alignment = Alignment.CenterVertically),
) {
- TonalButtonWithIcon("分享", Icons.Rounded.Share, onClickShare)
- TonalButtonWithIcon("下载", Icons.Rounded.Download, onClickDownload)
+ TonalButtonWithIcon(shareText, Icons.Rounded.Share, onClickShare)
+ TonalButtonWithIcon(downloadText, Icons.Rounded.Download, onClickDownload)
}
Box(Modifier.width(IntrinsicSize.Max)) {
diff --git a/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/danmaku/MatchingDanmakuDialogs.kt b/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/danmaku/MatchingDanmakuDialogs.kt
index d662ed73f7..a1841fa0ea 100644
--- a/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/danmaku/MatchingDanmakuDialogs.kt
+++ b/app/shared/ui-episode/src/commonMain/kotlin/ui/episode/danmaku/MatchingDanmakuDialogs.kt
@@ -36,9 +36,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.episode_danmaku_match_change
+import me.him188.ani.app.ui.lang.episode_danmaku_match_loading_danmaku
+import me.him188.ani.app.ui.lang.episode_danmaku_match_loading_subjects
+import me.him188.ani.app.ui.lang.episode_danmaku_match_select_episode
+import me.him188.ani.app.ui.lang.episode_danmaku_match_select_subject
+import me.him188.ani.app.ui.lang.exploration_search
+import me.him188.ani.app.ui.lang.settings_mediasource_cancel
+import me.him188.ani.app.ui.lang.settings_mediasource_test_keyword
import me.him188.ani.danmaku.api.provider.DanmakuEpisode
import me.him188.ani.danmaku.api.provider.DanmakuFetchResult
import me.him188.ani.danmaku.api.provider.DanmakuSubject
+import org.jetbrains.compose.resources.stringResource
@Composable
fun MatchingDanmakuDialog(
@@ -50,12 +60,14 @@ fun MatchingDanmakuDialog(
onSelectEpisode: (DanmakuEpisode) -> Unit,
onComplete: (List) -> Unit,
) {
+ val changeDanmakuText = stringResource(Lang.episode_danmaku_match_change)
+ val cancelText = stringResource(Lang.settings_mediasource_cancel)
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
},
title = {
- Text("更换弹幕")
+ Text(changeDanmakuText)
},
text = {
MatchingDanmakuScreen(
@@ -69,7 +81,7 @@ fun MatchingDanmakuDialog(
},
dismissButton = {
TextButton(onDismissRequest) {
- Text("取消")
+ Text(cancelText)
}
},
)
@@ -85,6 +97,10 @@ fun MatchingDanmakuScreen(
onSelectEpisode: (DanmakuEpisode) -> Unit,
onComplete: (List) -> Unit,
) {
+ val keywordText = stringResource(Lang.settings_mediasource_test_keyword)
+ val searchText = stringResource(Lang.exploration_search)
+ val loadingEpisodesText = stringResource(Lang.episode_danmaku_match_loading_subjects)
+ val loadingDanmakuText = stringResource(Lang.episode_danmaku_match_loading_danmaku)
if (uiState.isFlowComplete) {
onComplete(uiState.danmakuFetchResults)
return
@@ -122,7 +138,7 @@ fun MatchingDanmakuScreen(
OutlinedTextField(
value = query,
onValueChange = { query = it },
- label = { Text("关键词") },
+ label = { Text(keywordText) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
@@ -134,7 +150,7 @@ fun MatchingDanmakuScreen(
onClick = { onSubmitQuery(query) },
enabled = query.isNotBlank() && !uiState.isLoadingSubjects,
) {
- Text("搜索")
+ Text(searchText)
}
// Show a loading spinner if subjects are loading
@@ -152,7 +168,7 @@ fun MatchingDanmakuScreen(
// Loading/Errors for episodes and danmaku can also be displayed in-line, if desired.
if (uiState.isLoadingEpisodes) {
Spacer(modifier = Modifier.height(8.dp))
- Text("正在查询剧集列表…")
+ Text(loadingEpisodesText)
}
uiState.episodeError?.let { episodeErr ->
Spacer(modifier = Modifier.height(8.dp))
@@ -161,7 +177,7 @@ fun MatchingDanmakuScreen(
if (uiState.isLoadingDanmaku) {
Spacer(modifier = Modifier.height(8.dp))
- Text("正在查询弹幕列表…")
+ Text(loadingDanmakuText)
}
uiState.danmakuError?.let { danmakuErr ->
Spacer(modifier = Modifier.height(8.dp))
@@ -200,9 +216,11 @@ fun SubjectPickerDialog(
onSelect: (DanmakuSubject) -> Unit,
onDismissRequest: () -> Unit,
) {
+ val selectSubjectText = stringResource(Lang.episode_danmaku_match_select_subject)
+ val cancelText = stringResource(Lang.settings_mediasource_cancel)
AlertDialog(
onDismissRequest = onDismissRequest,
- title = { Text("选择条目") },
+ title = { Text(selectSubjectText) },
text = {
LazyColumn {
items(subjects, key = { "MatchingDanmakuDialog-" + it.id }, contentType = { 1 }) { subject ->
@@ -217,7 +235,7 @@ fun SubjectPickerDialog(
confirmButton = {
},
dismissButton = {
- TextButton(onClick = onDismissRequest) { Text("取消") }
+ TextButton(onClick = onDismissRequest) { Text(cancelText) }
},
)
}
@@ -228,9 +246,11 @@ fun EpisodePickerDialog(
onSelect: (DanmakuEpisode) -> Unit,
onDismissRequest: () -> Unit,
) {
+ val selectEpisodeText = stringResource(Lang.episode_danmaku_match_select_episode)
+ val cancelText = stringResource(Lang.settings_mediasource_cancel)
AlertDialog(
onDismissRequest = onDismissRequest,
- title = { Text("选择剧集") },
+ title = { Text(selectEpisodeText) },
text = {
LazyColumn {
items(episodes, key = { "MatchingDanmakuDialog-" + it.id }, contentType = { 1 }) { episode ->
@@ -247,7 +267,7 @@ fun EpisodePickerDialog(
confirmButton = {
},
dismissButton = {
- TextButton(onClick = onDismissRequest) { Text("取消") }
+ TextButton(onClick = onDismissRequest) { Text(cancelText) }
},
)
}
diff --git a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/followed/FollowedSubjectsLazyRow.kt b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/followed/FollowedSubjectsLazyRow.kt
index 0b1028b465..0cb21b020d 100644
--- a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/followed/FollowedSubjectsLazyRow.kt
+++ b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/followed/FollowedSubjectsLazyRow.kt
@@ -63,6 +63,9 @@ import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.minimumHairlineSize
import me.him188.ani.app.ui.foundation.stateOf
import me.him188.ani.app.ui.foundation.widgets.NsfwMask
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.exploration_followed_collect_as_to_show_here
+import me.him188.ani.app.ui.lang.subject_collection_doing
import me.him188.ani.app.ui.search.LoadErrorCard
import me.him188.ani.app.ui.search.LoadErrorCardLayout
import me.him188.ani.app.ui.search.LoadErrorCardRole
@@ -71,7 +74,9 @@ import me.him188.ani.app.ui.search.isLoadingFirstPage
import me.him188.ani.app.ui.search.rememberLoadErrorState
import me.him188.ani.app.ui.search.rememberTestLazyPagingItems
import me.him188.ani.app.ui.subject.SubjectProgressState
+import me.him188.ani.app.ui.subject.rememberSubjectStatusStrings
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
// https://www.figma.com/design/LET1n9mmDa6npDTIlUuJjU/Animeko?node-id=62-4581&node-type=frame&t=Evw0PwXZHXQNgEm3-0
@Composable
@@ -125,12 +130,16 @@ fun FollowedSubjectsLazyRow(
items.isFinishedAndEmpty -> {
item {
+ val followedHintText = stringResource(
+ Lang.exploration_followed_collect_as_to_show_here,
+ stringResource(Lang.subject_collection_doing),
+ )
Box(Modifier.minimumHairlineSize()) {
LoadErrorCardLayout(
LoadErrorCardRole.Unimportant,
content = {
ListItem(
- headlineContent = { Text("将番剧收藏为 \"在看\" 后将在这里显示") },
+ headlineContent = { Text(followedHintText) },
colors = listItemColors,
)
},
@@ -182,10 +191,11 @@ private fun FollowedSubjectItem(
modifier.placeholder(item == null, shape = shape),
supportingText = {
if (item != null) {
+ val strings = rememberSubjectStatusStrings()
val airingState = remember(item) {
SubjectProgressState(stateOf(item.subjectProgressInfo))
}
- Text(airingState.buttonText, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text(airingState.buttonText(strings), maxLines = 1, overflow = TextOverflow.Ellipsis)
}
},
maskShape = shape,
diff --git a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/schedule/ScheduleItem.kt b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/schedule/ScheduleItem.kt
index 5579d12897..c536d66b8a 100644
--- a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/schedule/ScheduleItem.kt
+++ b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/schedule/ScheduleItem.kt
@@ -24,7 +24,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -42,7 +41,12 @@ import kotlinx.datetime.format.char
import kotlinx.datetime.number
import me.him188.ani.app.ui.foundation.AsyncImage
import me.him188.ani.app.ui.foundation.layout.paddingIfNotEmpty
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.exploration_schedule_episode
+import me.him188.ani.app.ui.lang.exploration_schedule_episode_ep_and_sort
+import me.him188.ani.app.ui.lang.exploration_schedule_view_details
import me.him188.ani.datasources.api.EpisodeSort
+import org.jetbrains.compose.resources.stringResource
/**
* 新番时间表的一个项目.
@@ -100,7 +104,11 @@ fun ScheduleItem(
},
trailingContent = action,
colors = colors,
- modifier = modifier.clickable(role = Role.Button, onClickLabel = "查看详情", onClick = onClick),
+ modifier = modifier.clickable(
+ role = Role.Button,
+ onClickLabel = stringResource(Lang.exploration_schedule_view_details),
+ onClick = onClick,
+ ),
)
}
@@ -146,9 +154,23 @@ object ScheduleItemDefaults {
episodeName: String?,
modifier: Modifier = Modifier,
) {
- val text = remember(episodeSort) {
- renderEpisodeDisplay(episodeSort, episodeEp, episodeName)
+ val epText = episodeEp?.toString()?.removePrefix("0")
+ val sortText = episodeSort.toString().removePrefix("0")
+ val sortDisplay = if (episodeEp == null || episodeEp == episodeSort) {
+ if (episodeSort is EpisodeSort.Normal) {
+ stringResource(Lang.exploration_schedule_episode, sortText)
+ } else {
+ sortText
+ }
+ } else {
+ check(epText != null)
+ if (episodeSort is EpisodeSort.Normal && episodeEp is EpisodeSort.Normal) {
+ stringResource(Lang.exploration_schedule_episode_ep_and_sort, epText, sortText)
+ } else {
+ "$epText ($sortText)"
+ }
}
+ val text = if (episodeName == null) sortDisplay else "$sortDisplay $episodeName"
Text(
text,
overflow = TextOverflow.Ellipsis,
@@ -186,41 +208,9 @@ object ScheduleItemDefaults {
val timeString = timeFormatter.format(time)
return if (futureStartDate != null) {
- "${futureStartDate.month.number}/${futureStartDate.day} 起\n${timeString}"
+ "${futureStartDate.month.number}/${futureStartDate.day}\n${timeString}"
} else {
timeString
}
}
-
- // internal for testing
- internal fun renderEpisodeDisplay(
- episodeSort: EpisodeSort,
- episodeEp: EpisodeSort?,
- episodeName: String?
- ): String {
- val epText = episodeEp?.toString()?.removePrefix("0")
- val sortText = episodeSort.toString().removePrefix("0")
-
- val sortDisplay = if (episodeEp == null || episodeEp == episodeSort) {
- if (episodeSort is EpisodeSort.Normal) {
- "第 $sortText 话"
- } else {
- sortText
- }
- } else {
- check(epText != null)
- // episodeEp != episodeSort
- if (episodeSort is EpisodeSort.Normal && episodeEp is EpisodeSort.Normal) {
- "第 $epText ($sortText) 话"
- } else {
- "$epText ($sortText)"
- }
- }
-
- return if (episodeName == null) {
- sortDisplay
- } else {
- "$sortDisplay $episodeName"
- }
- }
}
diff --git a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/schedule/ScheduleScreen.kt b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/schedule/ScheduleScreen.kt
index 0cf32668fe..9e84956992 100644
--- a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/schedule/ScheduleScreen.kt
+++ b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/schedule/ScheduleScreen.kt
@@ -76,9 +76,22 @@ import me.him188.ani.app.ui.foundation.pagerTabIndicatorOffset
import me.him188.ani.app.ui.foundation.rememberHorizontalScrollControlState
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.exploration_schedule
+import me.him188.ani.app.ui.lang.exploration_schedule_last_weekday
+import me.him188.ani.app.ui.lang.exploration_schedule_next_weekday
+import me.him188.ani.app.ui.lang.exploration_schedule_this_weekday
+import me.him188.ani.app.ui.lang.exploration_schedule_weekday_friday
+import me.him188.ani.app.ui.lang.exploration_schedule_weekday_monday
+import me.him188.ani.app.ui.lang.exploration_schedule_weekday_saturday
+import me.him188.ani.app.ui.lang.exploration_schedule_weekday_sunday
+import me.him188.ani.app.ui.lang.exploration_schedule_weekday_thursday
+import me.him188.ani.app.ui.lang.exploration_schedule_weekday_tuesday
+import me.him188.ani.app.ui.lang.exploration_schedule_weekday_wednesday
import me.him188.ani.app.ui.search.LoadErrorCard
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.isDesktop
+import org.jetbrains.compose.resources.stringResource
fun ScheduleScreenState(
daysProvider: () -> List,
@@ -140,7 +153,7 @@ fun ScheduleScreen(
modifier,
topBar = {
AniTopAppBar(
- title = { Text("新番时间表") },
+ title = { Text(stringResource(Lang.exploration_schedule)) },
Modifier.fillMaxWidth(),
navigationIcon = navigationIcon,
windowInsets = windowInsets.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
@@ -381,6 +394,7 @@ object ScheduleScreenDefaults {
@Stable
+@Composable
private fun renderScheduleDay(day: ScheduleDay): String {
val date = day.date
return """
@@ -391,40 +405,25 @@ private fun renderScheduleDay(day: ScheduleDay): String {
@Stable
@Suppress("REDUNDANT_ELSE_IN_WHEN") // Compiler works fine, but IDE complains about this, so we suppress it.
-private fun renderDayOfWeek(day: DayOfWeek, kind: ScheduleDay.Kind): String = when (kind) {
- // we manually permute them to make them real constants to avoid runtime allocations.
- ScheduleDay.Kind.LAST_WEEK -> when (day) {
- DayOfWeek.MONDAY -> "上周一"
- DayOfWeek.TUESDAY -> "上周二"
- DayOfWeek.WEDNESDAY -> "上周三"
- DayOfWeek.THURSDAY -> "上周四"
- DayOfWeek.FRIDAY -> "上周五"
- DayOfWeek.SATURDAY -> "上周六"
- DayOfWeek.SUNDAY -> "上周日"
+@Composable
+private fun renderDayOfWeek(day: DayOfWeek, kind: ScheduleDay.Kind): String {
+ val weekday = when (day) {
+ DayOfWeek.MONDAY -> stringResource(Lang.exploration_schedule_weekday_monday)
+ DayOfWeek.TUESDAY -> stringResource(Lang.exploration_schedule_weekday_tuesday)
+ DayOfWeek.WEDNESDAY -> stringResource(Lang.exploration_schedule_weekday_wednesday)
+ DayOfWeek.THURSDAY -> stringResource(Lang.exploration_schedule_weekday_thursday)
+ DayOfWeek.FRIDAY -> stringResource(Lang.exploration_schedule_weekday_friday)
+ DayOfWeek.SATURDAY -> stringResource(Lang.exploration_schedule_weekday_saturday)
+ DayOfWeek.SUNDAY -> stringResource(Lang.exploration_schedule_weekday_sunday)
else -> day.toString()
}
- ScheduleDay.Kind.THIS_WEEK,
- ScheduleDay.Kind.TODAY -> when (day) {
- DayOfWeek.MONDAY -> "周一"
- DayOfWeek.TUESDAY -> "周二"
- DayOfWeek.WEDNESDAY -> "周三"
- DayOfWeek.THURSDAY -> "周四"
- DayOfWeek.FRIDAY -> "周五"
- DayOfWeek.SATURDAY -> "周六"
- DayOfWeek.SUNDAY -> "周日"
- else -> day.toString()
- }
+ return when (kind) {
+ ScheduleDay.Kind.LAST_WEEK -> stringResource(Lang.exploration_schedule_last_weekday, weekday)
+ ScheduleDay.Kind.THIS_WEEK,
+ ScheduleDay.Kind.TODAY -> stringResource(Lang.exploration_schedule_this_weekday, weekday)
- ScheduleDay.Kind.NEXT_WEEK -> when (day) {
- DayOfWeek.MONDAY -> "下周一"
- DayOfWeek.TUESDAY -> "下周二"
- DayOfWeek.WEDNESDAY -> "下周三"
- DayOfWeek.THURSDAY -> "下周四"
- DayOfWeek.FRIDAY -> "下周五"
- DayOfWeek.SATURDAY -> "下周六"
- DayOfWeek.SUNDAY -> "下周日"
- else -> day.toString()
+ ScheduleDay.Kind.NEXT_WEEK -> stringResource(Lang.exploration_schedule_next_weekday, weekday)
}
}
diff --git a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchFilter.kt b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchFilter.kt
index 8b0f5933da..905fa31912 100644
--- a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchFilter.kt
+++ b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchFilter.kt
@@ -41,7 +41,21 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.him188.ani.app.data.models.subject.CanonicalTagKind
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.exploration_search_filter_audience
+import me.him188.ani.app.ui.lang.exploration_search_filter_category
+import me.him188.ani.app.ui.lang.exploration_search_filter_character
+import me.him188.ani.app.ui.lang.exploration_search_filter_custom
+import me.him188.ani.app.ui.lang.exploration_search_filter_emotion
+import me.him188.ani.app.ui.lang.exploration_search_filter_genre
+import me.him188.ani.app.ui.lang.exploration_search_filter_rating
+import me.him188.ani.app.ui.lang.exploration_search_filter_region
+import me.him188.ani.app.ui.lang.exploration_search_filter_series
+import me.him188.ani.app.ui.lang.exploration_search_filter_setting
+import me.him188.ani.app.ui.lang.exploration_search_filter_source
+import me.him188.ani.app.ui.lang.exploration_search_filter_technology
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
import kotlin.random.Random
/**
@@ -102,6 +116,7 @@ fun SearchFilterChip(
onCheckedChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
+ val labels = rememberSearchFilterLabels()
var showDropdown by rememberSaveable { mutableStateOf(false) }
val textLayout = rememberTextMeasurer(1)
val density = LocalDensity.current
@@ -109,7 +124,7 @@ fun SearchFilterChip(
val maxWidth = remember(textLayout, density, styleLabelLarge) {
with(density) {
textLayout.measure(
- "占位,占位位",
+ "placeholder",
softWrap = false,
maxLines = 1,
style = styleLabelLarge,
@@ -122,7 +137,7 @@ fun SearchFilterChip(
onClick = { showDropdown = true },
label = {
Text(
- renderChipLabel(state),
+ renderChipLabel(state, labels),
Modifier.widthIn(max = maxWidth.coerceAtLeast(128.dp)),
overflow = TextOverflow.Ellipsis,
softWrap = false,
@@ -161,26 +176,59 @@ fun SearchFilterChip(
private fun renderChipLabel(
state: SearchFilterChipState,
+ labels: SearchFilterLabels,
): String {
if (state.hasSelection) {
return state.selected.joinToString(",")
}
return when (state.kind) {
- CanonicalTagKind.Audience -> "受众"
- CanonicalTagKind.Category -> "分类"
- CanonicalTagKind.Character -> "角色"
- CanonicalTagKind.Emotion -> "情感"
- CanonicalTagKind.Genre -> "类型"
- CanonicalTagKind.Rating -> "分级"
- CanonicalTagKind.Region -> "地区"
- CanonicalTagKind.Series -> "系列"
- CanonicalTagKind.Setting -> "设定"
- CanonicalTagKind.Source -> "来源"
- CanonicalTagKind.Technology -> "技术"
- null -> "自定义"
+ CanonicalTagKind.Audience -> labels.audience
+ CanonicalTagKind.Category -> labels.category
+ CanonicalTagKind.Character -> labels.character
+ CanonicalTagKind.Emotion -> labels.emotion
+ CanonicalTagKind.Genre -> labels.genre
+ CanonicalTagKind.Rating -> labels.rating
+ CanonicalTagKind.Region -> labels.region
+ CanonicalTagKind.Series -> labels.series
+ CanonicalTagKind.Setting -> labels.setting
+ CanonicalTagKind.Source -> labels.source
+ CanonicalTagKind.Technology -> labels.technology
+ null -> labels.custom
}
}
+@Immutable
+private data class SearchFilterLabels(
+ val audience: String,
+ val category: String,
+ val character: String,
+ val emotion: String,
+ val genre: String,
+ val rating: String,
+ val region: String,
+ val series: String,
+ val setting: String,
+ val source: String,
+ val technology: String,
+ val custom: String,
+)
+
+@Composable
+private fun rememberSearchFilterLabels(): SearchFilterLabels = SearchFilterLabels(
+ audience = stringResource(Lang.exploration_search_filter_audience),
+ category = stringResource(Lang.exploration_search_filter_category),
+ character = stringResource(Lang.exploration_search_filter_character),
+ emotion = stringResource(Lang.exploration_search_filter_emotion),
+ genre = stringResource(Lang.exploration_search_filter_genre),
+ rating = stringResource(Lang.exploration_search_filter_rating),
+ region = stringResource(Lang.exploration_search_filter_region),
+ series = stringResource(Lang.exploration_search_filter_series),
+ setting = stringResource(Lang.exploration_search_filter_setting),
+ source = stringResource(Lang.exploration_search_filter_source),
+ technology = stringResource(Lang.exploration_search_filter_technology),
+ custom = stringResource(Lang.exploration_search_filter_custom),
+)
+
@OptIn(TestOnly::class)
@Composable
diff --git a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchPage.kt b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchPage.kt
index 0f6964a74e..4df8fd1545 100644
--- a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchPage.kt
+++ b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchPage.kt
@@ -95,10 +95,15 @@ import me.him188.ani.app.ui.foundation.layout.plus
import me.him188.ani.app.ui.foundation.navigation.BackHandler
import me.him188.ani.app.ui.foundation.preview.PreviewSizeClasses
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.exploration_search
+import me.him188.ani.app.ui.lang.exploration_search_back_to_top
+import me.him188.ani.app.ui.lang.settings_mediasource_test_keyword
import me.him188.ani.app.ui.search.TestSearchState
import me.him188.ani.app.ui.search.collectItemsWithLifecycle
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.isDesktop
+import org.jetbrains.compose.resources.stringResource
@Composable
fun SearchPage(
@@ -112,6 +117,9 @@ fun SearchPage(
contentWindowInsets: WindowInsets = AniWindowInsets.forPageContent(),
navigationIcon: @Composable () -> Unit = {},
) {
+ val searchText = stringResource(Lang.exploration_search)
+ val keywordText = stringResource(Lang.settings_mediasource_test_keyword)
+ val backToTopText = stringResource(Lang.exploration_search_back_to_top)
val coroutineScope = rememberCoroutineScope()
val items = state.searchState.collectItemsWithLifecycle()
val focusManager = LocalFocusManager.current
@@ -145,7 +153,7 @@ fun SearchPage(
expanded = isSearchBarExpanded,
onExpandedChange = { isSearchBarExpanded = it },
modifier = Modifier.padding(bottom = 16.dp),
- placeholder = { Text("关键词") },
+ placeholder = { Text(keywordText) },
windowInsets = contentWindowInsets.only(WindowInsetsSides.Horizontal),
)
},
@@ -273,7 +281,7 @@ fun SearchPage(
}
},
) {
- Icon(Icons.Rounded.KeyboardArrowUp, "回到顶部")
+ Icon(Icons.Rounded.KeyboardArrowUp, backToTopText)
}
}
},
@@ -457,6 +465,7 @@ internal fun SearchPageListDetailScaffold(
contentWindowInsets: WindowInsets = AniWindowInsets.forPageContent(),
) {
val coroutineScope = rememberCoroutineScope()
+ val searchText = stringResource(Lang.exploration_search)
val topAppBarScrollBehavior: TopAppBarScrollBehavior? = if (LocalPlatform.current.isDesktop()) {
null
@@ -468,7 +477,7 @@ internal fun SearchPageListDetailScaffold(
navigator = navigator,
listPaneTopAppBar = {
AniTopAppBar(
- title = { Text("搜索") },
+ title = { Text(searchText) },
modifier = Modifier.fillMaxWidth(),
navigationIcon = {
if (navigator.canNavigateBack()) {
diff --git a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchPageResultColumn.kt b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchPageResultColumn.kt
index 612d48356b..d641262c17 100644
--- a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchPageResultColumn.kt
+++ b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SearchPageResultColumn.kt
@@ -42,6 +42,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
@@ -78,6 +79,13 @@ import me.him188.ani.app.ui.foundation.layout.paneHorizontalPadding
import me.him188.ani.app.ui.foundation.layout.paneVerticalPadding
import me.him188.ani.app.ui.foundation.widgets.NsfwMask
import me.him188.ani.app.ui.foundation.widgets.SelectableDropdownMenuItem
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.exploration_search_results_shown
+import me.him188.ani.app.ui.lang.exploration_search_sort_collection
+import me.him188.ani.app.ui.lang.exploration_search_sort_date
+import me.him188.ani.app.ui.lang.exploration_search_sort_match
+import me.him188.ani.app.ui.lang.exploration_search_sort_rank
+import me.him188.ani.app.ui.lang.foundation_load_error_no_results
import me.him188.ani.app.ui.search.LoadErrorCard
import me.him188.ani.app.ui.search.SearchDefaults.IconTextButton
import me.him188.ani.app.ui.search.SearchResultLazyVerticalGrid
@@ -86,6 +94,7 @@ import me.him188.ani.app.ui.search.isFinishedAndEmpty
import me.him188.ani.app.ui.subject.SubjectCoverCard
import me.him188.ani.app.ui.subject.SubjectGridDefaults
import me.him188.ani.app.ui.subject.SubjectGridLayoutParams
+import org.jetbrains.compose.resources.stringResource
@Composable
@@ -317,10 +326,11 @@ private fun LazyGridItemScope.SearchResultColumnScopeImpl(
modifier: Modifier
) {
val modifier1 = modifier // 不要加动画, #1901
+ val noResultsText = stringResource(Lang.foundation_load_error_no_results)
when {
itemsState.value.isFinishedAndEmpty -> {
ListItem(
- headlineContent = { Text("无搜索结果") },
+ headlineContent = { Text(noResultsText) },
modifier = modifier1,
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest),
)
@@ -333,7 +343,10 @@ private fun LazyGridItemScope.SearchResultColumnScopeImpl(
verticalArrangement = Arrangement.aligned(Alignment.CenterVertically),
itemVerticalAlignment = Alignment.CenterVertically,
) {
- Text("已显示 ${itemsState.value.itemCount} 个结果", style = MaterialTheme.typography.bodyLarge)
+ Text(
+ stringResource(Lang.exploration_search_results_shown, itemsState.value.itemCount),
+ style = MaterialTheme.typography.bodyLarge,
+ )
Row(
Modifier.weight(1f).align(Alignment.Bottom),
verticalAlignment = Alignment.CenterVertically,
@@ -403,6 +416,7 @@ private fun SortButton(
onSortChange: (SearchSort) -> Unit,
modifier: Modifier = Modifier,
) {
+ val sortLabels = rememberSearchSortLabels()
Box(
modifier, contentAlignment = Alignment.BottomEnd,
) {
@@ -415,7 +429,7 @@ private fun SortButton(
Icon(Icons.AutoMirrored.Rounded.Sort, null)
},
) {
- Text(getSortText(currentSort), softWrap = false)
+ Text(getSortText(currentSort, sortLabels), softWrap = false)
}
DropdownMenu(showDropdown, { showDropdown = false }) {
for (sort in SearchSort.entries) {
@@ -423,7 +437,7 @@ private fun SortButton(
selected = sort == currentSort,
text = {
Text(
- getSortText(sort),
+ getSortText(sort, sortLabels),
softWrap = false,
)
},
@@ -437,9 +451,25 @@ private fun SortButton(
}
}
-private fun getSortText(currentSort: SearchSort): String = when (currentSort) {
- SearchSort.MATCH -> "最佳匹配"
- SearchSort.COLLECTION -> "最多收藏"
- SearchSort.RANK -> "最高排名"
- SearchSort.DATE -> "发布日期"
+@Immutable
+private data class SearchSortLabels(
+ val match: String,
+ val collection: String,
+ val rank: String,
+ val date: String,
+)
+
+@Composable
+private fun rememberSearchSortLabels(): SearchSortLabels = SearchSortLabels(
+ match = stringResource(Lang.exploration_search_sort_match),
+ collection = stringResource(Lang.exploration_search_sort_collection),
+ rank = stringResource(Lang.exploration_search_sort_rank),
+ date = stringResource(Lang.exploration_search_sort_date),
+)
+
+private fun getSortText(currentSort: SearchSort, labels: SearchSortLabels): String = when (currentSort) {
+ SearchSort.MATCH -> labels.match
+ SearchSort.COLLECTION -> labels.collection
+ SearchSort.RANK -> labels.rank
+ SearchSort.DATE -> labels.date
}
diff --git a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SubjectItem.kt b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SubjectItem.kt
index 8a9014d4b6..acc9d3090d 100644
--- a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SubjectItem.kt
+++ b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SubjectItem.kt
@@ -48,6 +48,9 @@ import me.him188.ani.app.ui.foundation.AsyncImage
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.layout.isHeightAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_management_play
+import org.jetbrains.compose.resources.stringResource
/**
* Design: [SubjectItem on Figma](https://www.figma.com/design/LET1n9mmDa6npDTIlUuJjU/Main?node-id=101-877&t=gmFJS6LFQudIIXfK-4)
@@ -147,8 +150,9 @@ object SubjectItemDefaults {
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
+ val playText = stringResource(Lang.cache_management_play)
FilledTonalIconButton(onClick, modifier) {
- Icon(Icons.Rounded.PlayArrow, contentDescription = "播放", Modifier.size(28.dp))
+ Icon(Icons.Rounded.PlayArrow, contentDescription = playText, Modifier.size(28.dp))
}
}
diff --git a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SubjectPreviewItem.kt b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SubjectPreviewItem.kt
index 83029b4581..b7ea12e798 100644
--- a/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SubjectPreviewItem.kt
+++ b/app/shared/ui-exploration/src/commonMain/kotlin/ui/exploration/search/SubjectPreviewItem.kt
@@ -25,8 +25,8 @@ import me.him188.ani.app.data.models.subject.CanonicalTagKind
import me.him188.ani.app.data.models.subject.RatingCounts
import me.him188.ani.app.data.models.subject.RatingInfo
import me.him188.ani.app.data.models.subject.SubjectAiringInfo
+import me.him188.ani.app.data.models.subject.SubjectAiringKind
import me.him188.ani.app.data.models.subject.SubjectInfo
-import me.him188.ani.app.data.models.subject.computeTotalEpisodeText
import me.him188.ani.app.data.models.subject.kind
import me.him188.ani.app.data.models.subject.nameCnOrName
import me.him188.ani.app.data.network.LightRelatedCharacterInfo
@@ -34,9 +34,14 @@ import me.him188.ani.app.data.network.LightRelatedPersonInfo
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.layout.paneVerticalPadding
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.exploration_search_staff_prefix
+import me.him188.ani.app.ui.lang.subject_airing_total_episodes_completed
+import me.him188.ani.app.ui.lang.subject_airing_total_episodes_scheduled
import me.him188.ani.app.ui.rating.RatingText
-import me.him188.ani.app.ui.subject.renderSubjectSeason
+import me.him188.ani.app.ui.subject.getSubjectSeasonText
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.getString
@Immutable
class SubjectPreviewItemInfo(
@@ -61,7 +66,7 @@ class SubjectPreviewItemInfo(
/**
* @param nsfwModeSettings 用户设置的 NSFW 显示模式
*/
- fun compute(
+ suspend fun compute(
subjectInfo: SubjectInfo,
mainEpisodeCount: Int,
nsfwModeSettings: NsfwMode,
@@ -73,11 +78,11 @@ class SubjectPreviewItemInfo(
val airingInfo = SubjectAiringInfo.computeFromSubjectInfo(subjectInfo, mainEpisodeCount)
val tags = buildString {
if (subjectInfo.airDate.isValid) {
- append(renderSubjectSeason(subjectInfo.airDate))
+ append(getSubjectSeasonText(subjectInfo.airDate))
append(" · ")
}
- airingInfo.computeTotalEpisodeText()?.let {
- append("全 $mainEpisodeCount 话")
+ renderTotalEpisodesText(airingInfo)?.let {
+ append(it)
append(" · ")
}
@@ -106,7 +111,7 @@ class SubjectPreviewItemInfo(
if (persons.isEmpty()) return@let null
buildString {
- append("制作: ")
+ append(getString(Lang.exploration_search_staff_prefix))
persons.forEachIndexed { index, relatedPersonInfo ->
append(relatedPersonInfo.name)
if (index != persons.lastIndex) {
@@ -149,6 +154,21 @@ class SubjectPreviewItemInfo(
hide = hide,
)
}
+
+ private suspend fun renderTotalEpisodesText(airingInfo: SubjectAiringInfo): String? {
+ if (airingInfo.kind == SubjectAiringKind.UPCOMING && airingInfo.mainEpisodeCount == 0) {
+ return null
+ }
+ return when (airingInfo.kind) {
+ SubjectAiringKind.COMPLETED ->
+ getString(Lang.subject_airing_total_episodes_completed, airingInfo.mainEpisodeCount.toString())
+
+ SubjectAiringKind.UPCOMING,
+ SubjectAiringKind.ON_AIR,
+ ->
+ getString(Lang.subject_airing_total_episodes_scheduled, airingInfo.mainEpisodeCount.toString())
+ }
+ }
}
}
diff --git a/app/shared/ui-exploration/src/commonTest/kotlin/ui/exploration/schedule/EpisodeWithAiringTimeDefaultsTest.kt b/app/shared/ui-exploration/src/commonTest/kotlin/ui/exploration/schedule/EpisodeWithAiringTimeDefaultsTest.kt
index c48cd62a11..85f6e148c8 100644
--- a/app/shared/ui-exploration/src/commonTest/kotlin/ui/exploration/schedule/EpisodeWithAiringTimeDefaultsTest.kt
+++ b/app/shared/ui-exploration/src/commonTest/kotlin/ui/exploration/schedule/EpisodeWithAiringTimeDefaultsTest.kt
@@ -9,7 +9,6 @@
package me.him188.ani.app.ui.exploration.schedule
-import me.him188.ani.app.ui.exploration.schedule.ScheduleItemDefaults.renderEpisodeDisplay
import me.him188.ani.datasources.api.EpisodeSort
import me.him188.ani.datasources.api.EpisodeType
import kotlin.test.Test
@@ -70,3 +69,33 @@ class EpisodeWithAiringTimeDefaultsTest {
)
}
}
+
+private fun renderEpisodeDisplay(
+ episodeSort: EpisodeSort,
+ episodeEp: EpisodeSort?,
+ episodeName: String?
+): String {
+ val epText = episodeEp?.toString()?.removePrefix("0")
+ val sortText = episodeSort.toString().removePrefix("0")
+
+ val sortDisplay = if (episodeEp == null || episodeEp == episodeSort) {
+ if (episodeSort is EpisodeSort.Normal) {
+ "第 $sortText 话"
+ } else {
+ sortText
+ }
+ } else {
+ check(epText != null)
+ if (episodeSort is EpisodeSort.Normal && episodeEp is EpisodeSort.Normal) {
+ "第 $epText ($sortText) 话"
+ } else {
+ "$epText ($sortText)"
+ }
+ }
+
+ return if (episodeName == null) {
+ sortDisplay
+ } else {
+ "$sortDisplay $episodeName"
+ }
+}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/platform/navigation/BrowserNavigator.kt b/app/shared/ui-foundation/src/commonMain/kotlin/platform/navigation/BrowserNavigator.kt
index 572ffbbba5..437e075b0f 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/platform/navigation/BrowserNavigator.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/platform/navigation/BrowserNavigator.kt
@@ -20,8 +20,11 @@ import me.him188.ani.app.platform.Context
import me.him188.ani.app.ui.foundation.rememberAsyncHandler
import me.him188.ani.app.ui.foundation.setClipEntryText
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_browser_open_failed_copied
import me.him188.ani.utils.logging.error
import me.him188.ani.utils.logging.logger
+import org.jetbrains.compose.resources.stringResource
/**
* Please use [rememberAsyncBrowserNavigator] instead of this directly.
@@ -45,11 +48,13 @@ fun rememberAsyncBrowserNavigator(): BrowserNavigator {
val toaster = LocalToaster.current
val clipboard = LocalClipboard.current
val scope = rememberAsyncHandler()
+ val openFailedCopiedText = stringResource(Lang.foundation_browser_open_failed_copied)
- val failureAction: suspend (OpenBrowserResult.Failure) -> Unit = remember(clipboard, toaster) {
+ val failureAction: suspend (OpenBrowserResult.Failure) -> Unit =
+ remember(clipboard, toaster, openFailedCopiedText) {
{ failure ->
clipboard.setClipEntryText(failure.dest)
- toaster.toast("无法打开链接,已将链接复制到剪贴板,请打开浏览器访问")
+ toaster.toast(openFailedCopiedText)
logger.error(failure.throwable) { "Failed to open ${failure.dest}" }
}
}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/feedback/ErrorMessage.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/feedback/ErrorMessage.kt
index c1578213d9..764d663ba5 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/feedback/ErrorMessage.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/feedback/ErrorMessage.kt
@@ -36,7 +36,14 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import me.him188.ani.app.ui.foundation.setClipEntryText
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_error_dialog_copy_prefix
+import me.him188.ani.app.ui.lang.foundation_error_dialog_default_message
+import me.him188.ani.app.ui.lang.settings_danmaku_cancel
+import me.him188.ani.app.ui.lang.settings_danmaku_confirm
+import me.him188.ani.app.ui.lang.settings_mediasource_copy
import me.him188.ani.app.ui.loading.ConnectingDialog
+import org.jetbrains.compose.resources.stringResource
import kotlin.time.Duration.Companion.seconds
@@ -125,6 +132,11 @@ fun ErrorDialogHost(
val controller = remember {
StateErrorDialogController()
}
+ val defaultErrorMessage = stringResource(Lang.foundation_error_dialog_default_message)
+ val copyPrefixText = stringResource(Lang.foundation_error_dialog_copy_prefix)
+ val copyText = stringResource(Lang.settings_mediasource_copy)
+ val confirmText = stringResource(Lang.settings_danmaku_confirm)
+ val cancelText = stringResource(Lang.settings_danmaku_cancel)
val error = remember(errorFlow) {
errorFlow.distinctUntilChanged()
@@ -142,7 +154,7 @@ fun ErrorDialogHost(
if (controller.isVisible) {
ConnectingDialog(
text = {
- Text(text = error?.message ?: "Operation failed, please try again")
+ Text(text = error?.message ?: defaultErrorMessage)
val cause = error?.cause
if (cause != null) {
Column(Modifier.heightIn(max = 360.dp).verticalScroll(rememberScrollState())) {
@@ -167,13 +179,14 @@ fun ErrorDialogHost(
val scope = rememberCoroutineScope()
TextButton(
onClick = {
- val copyTarget = "删除缓存失败\n\n" + error.cause?.stackTraceToString()
+ val copyTarget =
+ (error?.message ?: copyPrefixText) + "\n\n" + error.cause?.stackTraceToString()
scope.launch {
clipboard.setClipEntryText(copyTarget)
}
},
) {
- Text("复制")
+ Text(copyText)
}
}
TextButton(
@@ -183,7 +196,7 @@ fun ErrorDialogHost(
onConfirm()
},
) {
- Text("OK")
+ Text(confirmText)
}
} else {
// recovering
@@ -194,7 +207,7 @@ fun ErrorDialogHost(
onClickCancel()
},
) {
- Text("取消")
+ Text(cancelText)
}
}
},
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/icons/PlayingIcon.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/icons/PlayingIcon.kt
index b7f4002568..75f77d3ef6 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/icons/PlayingIcon.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/icons/PlayingIcon.kt
@@ -25,6 +25,9 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_episode_now_playing
+import org.jetbrains.compose.resources.stringResource
/**
* 三个矩形垂直运动 (高度变化)
@@ -32,12 +35,13 @@ import androidx.compose.ui.unit.max
@Composable
fun PlayingIcon(
modifier: Modifier = Modifier,
- contentDescription: String = "正在播放",
+ contentDescription: String? = null,
width: Dp = 24.dp,
height: Dp = 16.dp,
thickness: Dp = 3.dp,
color: Color = LocalContentColor.current,
) {
+ val resolvedContentDescription = contentDescription ?: stringResource(Lang.subject_episode_now_playing)
val density = LocalDensity.current
val totalHeightPx = with(density) { height.toPx() }
val reservedHeightPx = totalHeightPx * (1 - 0.612f)
@@ -85,7 +89,7 @@ fun PlayingIcon(
) {
Canvas(
Modifier.height(height).width(width),
- contentDescription,
+ resolvedContentDescription,
) {
// draw there vertical lines, separated evenly
drawRoundRect(
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/lists/PaginatedList.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/lists/PaginatedList.kt
index 42d2290c20..8e1defaa0a 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/lists/PaginatedList.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/lists/PaginatedList.kt
@@ -40,6 +40,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_paginated_list_next_group
+import me.him188.ani.app.ui.lang.foundation_paginated_list_previous_group
+import me.him188.ani.app.ui.lang.foundation_paginated_list_select_group
+import org.jetbrains.compose.resources.stringResource
/**
* 通用分页列表组件,支持分组显示和分页导航
@@ -111,6 +116,9 @@ private fun PaginatedListNavigation(
modifier: Modifier = Modifier,
) {
var showGroupSelector by rememberSaveable { mutableStateOf(false) }
+ val previousGroupText = stringResource(Lang.foundation_paginated_list_previous_group)
+ val selectGroupText = stringResource(Lang.foundation_paginated_list_select_group)
+ val nextGroupText = stringResource(Lang.foundation_paginated_list_next_group)
Row(
modifier = modifier,
@@ -124,7 +132,7 @@ private fun PaginatedListNavigation(
) {
Icon(
Icons.Outlined.ChevronLeft,
- contentDescription = "上一组",
+ contentDescription = previousGroupText,
tint = if (state.canNavigateToPreviousGroup) {
MaterialTheme.colorScheme.onSurface
} else {
@@ -158,7 +166,7 @@ private fun PaginatedListNavigation(
)
Icon(
Icons.Outlined.ArrowDropDown,
- contentDescription = "选择分组",
+ contentDescription = selectGroupText,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@@ -197,7 +205,7 @@ private fun PaginatedListNavigation(
) {
Icon(
Icons.Outlined.ChevronRight,
- contentDescription = "下一组",
+ contentDescription = nextGroupText,
tint = if (state.canNavigateToNextGroup) {
MaterialTheme.colorScheme.onSurface
} else {
@@ -224,4 +232,4 @@ fun DefaultGroupHeader(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/session/SelfAvatar.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/session/SelfAvatar.kt
index 25decaf5ea..73ed4fb3e0 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/session/SelfAvatar.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/session/SelfAvatar.kt
@@ -41,7 +41,14 @@ import me.him188.ani.app.navigation.LocalNavigator
import me.him188.ani.app.tools.rememberUiMonoTasker
import me.him188.ani.app.ui.external.placeholder.placeholder
import me.him188.ani.app.ui.foundation.avatar.AvatarImage
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.login_sign_in
+import me.him188.ani.app.ui.lang.settings_account_confirm_logout
+import me.him188.ani.app.ui.lang.settings_account_logout
+import me.him188.ani.app.ui.lang.settings_account_settings
+import me.him188.ani.app.ui.lang.subject_collection_cancel
import me.him188.ani.app.ui.user.SelfInfoUiState
+import org.jetbrains.compose.resources.stringResource
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.CoroutineContext
@@ -53,6 +60,8 @@ fun SelfAvatar(
onClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
+ val signInText = stringResource(Lang.login_sign_in)
+
@Composable
fun Content(onClick: () -> Unit) {
if (state.isLoading) {
@@ -64,7 +73,7 @@ fun SelfAvatar(
} else {
if (state.isSessionValid == false || state.selfInfo == null) {
TextButton(onClick) {
- Text("登录")
+ Text(signInText)
}
} else {
AvatarImage(
@@ -120,8 +129,13 @@ private fun SelfAvatarMenus(
handler: SelfAvatarActionHandler,
onClickAny: () -> Unit,
) {
+ val settingsText = stringResource(Lang.settings_account_settings)
+ val logoutText = stringResource(Lang.settings_account_logout)
+ val confirmLogoutText = stringResource(Lang.settings_account_confirm_logout)
+ val cancelText = stringResource(Lang.subject_collection_cancel)
+
DropdownMenuItem(
- text = { Text("设置") },
+ text = { Text(settingsText) },
onClick = {
handler.onClickSettings()
onClickAny()
@@ -133,7 +147,7 @@ private fun SelfAvatarMenus(
var showLogoutConfirmation by rememberSaveable { mutableStateOf(false) }
val running by logoutTasker.isRunning.collectAsStateWithLifecycle()
DropdownMenuItem(
- text = { Text("退出登录", color = MaterialTheme.colorScheme.error) },
+ text = { Text(logoutText, color = MaterialTheme.colorScheme.error) },
leadingIcon = { Icon(Icons.AutoMirrored.Rounded.Logout, null) },
onClick = { showLogoutConfirmation = true },
enabled = !running,
@@ -141,7 +155,7 @@ private fun SelfAvatarMenus(
if (showLogoutConfirmation) {
AlertDialog(
{ showLogoutConfirmation = false },
- text = { Text("确定要退出登录吗?") },
+ text = { Text(confirmLogoutText) },
confirmButton = {
TextButton(
{
@@ -152,12 +166,12 @@ private fun SelfAvatarMenus(
showLogoutConfirmation = false
},
) {
- Text("退出登录", color = MaterialTheme.colorScheme.error)
+ Text(logoutText, color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton({ showLogoutConfirmation = false }) {
- Text("取消")
+ Text(cancelText)
}
},
)
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/widgets/NsfwMask.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/widgets/NsfwMask.kt
index 7fce4fb585..dc084d549e 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/widgets/NsfwMask.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/widgets/NsfwMask.kt
@@ -34,6 +34,10 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import me.him188.ani.app.data.models.preference.NsfwMode
import me.him188.ani.app.ui.foundation.effects.blurEffect
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_nsfw_hidden
+import me.him188.ani.app.ui.lang.foundation_nsfw_temporary_display
+import org.jetbrains.compose.resources.stringResource
/**
* Nsfw 模糊遮罩加提示. 点击可以临时展示.
@@ -67,9 +71,12 @@ fun NsfwMask(
verticalArrangement = Arrangement.Center,
modifier = Modifier.matchParentSize().defaultMinSize(minHeight = 30.dp).padding(top = 10.dp),
) {
- Text("此内容不适合展示", textAlign = TextAlign.Center)
+ Text(stringResource(Lang.foundation_nsfw_hidden), textAlign = TextAlign.Center)
IconButton(onTemporarilyDisplay) {
- Icon(Icons.Rounded.RemoveRedEye, contentDescription = "临时展示")
+ Icon(
+ Icons.Rounded.RemoveRedEye,
+ contentDescription = stringResource(Lang.foundation_nsfw_temporary_display),
+ )
}
}
}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/widgets/Toast.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/widgets/Toast.kt
index cbb9496abc..efec68ae0f 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/widgets/Toast.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/foundation/widgets/Toast.kt
@@ -33,16 +33,28 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
import me.him188.ani.app.domain.foundation.LoadError
import me.him188.ani.app.tools.MonoTasker
import me.him188.ani.app.ui.foundation.AbstractViewModel
import me.him188.ani.app.ui.foundation.animation.AniAnimatedVisibility
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_load_error_network
+import me.him188.ani.app.ui.lang.foundation_load_error_no_results
+import me.him188.ani.app.ui.lang.foundation_load_error_rate_limited
+import me.him188.ani.app.ui.lang.foundation_load_error_request_error
+import me.him188.ani.app.ui.lang.foundation_load_error_requires_login
+import me.him188.ani.app.ui.lang.foundation_load_error_service_unavailable
+import me.him188.ani.app.ui.lang.foundation_load_error_unknown_feedback
import me.him188.ani.utils.logging.logger
import me.him188.ani.utils.logging.warn
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.getString
import kotlin.math.max
import kotlin.math.min
@@ -63,26 +75,38 @@ interface Toaster {
private val logger = logger()
-fun Toaster.showLoadError(error: LoadError) { // TODO: localize
+@OptIn(DelicateCoroutinesApi::class)
+fun Toaster.showLoadError(error: LoadError) {
+ GlobalScope.launch {
+ show(renderLoadErrorToastMessage(error))
+ }
+
when (error) {
- LoadError.NetworkError -> show("网络错误,请检查网络连接或稍后再试")
- LoadError.NoResults -> show("没有结果")
- LoadError.RateLimited -> show("请求过于频繁,请稍后再试")
- LoadError.RequiresLogin -> show("此功能需要登录")
- LoadError.ServiceUnavailable -> show("服务不可用,请稍后再试")
is LoadError.UnknownError -> {
- show("发生未知错误,请在设置中反馈(附加日志)")
logger.warn(error.throwable) {
"Toaster showing an LoadError.UnknownError"
}
}
is LoadError.RequestError -> {
- show("请求错误: ${error.localized}")
logger.warn {
"Toaster showing an LoadError.RequestError: ${error.localized}"
}
}
+
+ else -> {}
+ }
+}
+
+private suspend fun renderLoadErrorToastMessage(error: LoadError): String {
+ return when (error) {
+ LoadError.NetworkError -> getString(Lang.foundation_load_error_network)
+ LoadError.NoResults -> getString(Lang.foundation_load_error_no_results)
+ LoadError.RateLimited -> getString(Lang.foundation_load_error_rate_limited)
+ LoadError.RequiresLogin -> getString(Lang.foundation_load_error_requires_login)
+ LoadError.ServiceUnavailable -> getString(Lang.foundation_load_error_service_unavailable)
+ is LoadError.UnknownError -> getString(Lang.foundation_load_error_unknown_feedback)
+ is LoadError.RequestError -> getString(Lang.foundation_load_error_request_error, error.localized)
}
}
@@ -156,4 +180,4 @@ fun Toast(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/media/MediaDetailsRenderer.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/media/MediaDetailsRenderer.kt
index 50b0433f0a..c64c12b42a 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/media/MediaDetailsRenderer.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/media/MediaDetailsRenderer.kt
@@ -1,27 +1,103 @@
package me.him188.ani.app.ui.media
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_unknown
+import me.him188.ani.app.ui.lang.media_subtitle_kind_closed
+import me.him188.ani.app.ui.lang.media_subtitle_kind_closed_or_external_discovered
+import me.him188.ani.app.ui.lang.media_subtitle_kind_embedded
+import me.him188.ani.app.ui.lang.media_subtitle_kind_external_discovered
+import me.him188.ani.app.ui.lang.media_subtitle_kind_external_provided
+import me.him188.ani.app.ui.lang.media_subtitle_language_chinese_cantonese
+import me.him188.ani.app.ui.lang.media_subtitle_language_chinese_simplified
+import me.him188.ani.app.ui.lang.media_subtitle_language_chinese_traditional
+import me.him188.ani.app.ui.lang.media_subtitle_language_english
+import me.him188.ani.app.ui.lang.media_subtitle_language_japanese
import me.him188.ani.datasources.api.Media
import me.him188.ani.datasources.api.SubtitleKind
import me.him188.ani.datasources.api.topic.FileSize.Companion.Unspecified
import me.him188.ani.datasources.api.topic.FileSize.Companion.bytes
import me.him188.ani.datasources.api.topic.Resolution
import me.him188.ani.datasources.api.topic.SubtitleLanguage
+import org.jetbrains.compose.resources.stringResource
import kotlin.jvm.JvmName
+@Immutable
+class MediaDetailsStrings(
+ val subtitleKindEmbedded: String,
+ val subtitleKindClosed: String,
+ val subtitleKindExternalProvided: String,
+ val subtitleKindExternalDiscovered: String,
+ val subtitleKindClosedOrExternalDiscovered: String,
+ val unknown: String,
+ val subtitleLanguageChineseCantonese: String,
+ val subtitleLanguageChineseSimplified: String,
+ val subtitleLanguageChineseTraditional: String,
+ val subtitleLanguageJapanese: String,
+ val subtitleLanguageEnglish: String,
+)
+
+@Composable
+fun rememberMediaDetailsStrings(): MediaDetailsStrings {
+ val subtitleKindEmbedded = stringResource(Lang.media_subtitle_kind_embedded)
+ val subtitleKindClosed = stringResource(Lang.media_subtitle_kind_closed)
+ val subtitleKindExternalProvided = stringResource(Lang.media_subtitle_kind_external_provided)
+ val subtitleKindExternalDiscovered = stringResource(Lang.media_subtitle_kind_external_discovered)
+ val subtitleKindClosedOrExternalDiscovered = stringResource(Lang.media_subtitle_kind_closed_or_external_discovered)
+ val unknown = stringResource(Lang.cache_unknown)
+ val subtitleLanguageChineseCantonese = stringResource(Lang.media_subtitle_language_chinese_cantonese)
+ val subtitleLanguageChineseSimplified = stringResource(Lang.media_subtitle_language_chinese_simplified)
+ val subtitleLanguageChineseTraditional = stringResource(Lang.media_subtitle_language_chinese_traditional)
+ val subtitleLanguageJapanese = stringResource(Lang.media_subtitle_language_japanese)
+ val subtitleLanguageEnglish = stringResource(Lang.media_subtitle_language_english)
+
+ return remember(
+ subtitleKindEmbedded,
+ subtitleKindClosed,
+ subtitleKindExternalProvided,
+ subtitleKindExternalDiscovered,
+ subtitleKindClosedOrExternalDiscovered,
+ unknown,
+ subtitleLanguageChineseCantonese,
+ subtitleLanguageChineseSimplified,
+ subtitleLanguageChineseTraditional,
+ subtitleLanguageJapanese,
+ subtitleLanguageEnglish,
+ ) {
+ MediaDetailsStrings(
+ subtitleKindEmbedded = subtitleKindEmbedded,
+ subtitleKindClosed = subtitleKindClosed,
+ subtitleKindExternalProvided = subtitleKindExternalProvided,
+ subtitleKindExternalDiscovered = subtitleKindExternalDiscovered,
+ subtitleKindClosedOrExternalDiscovered = subtitleKindClosedOrExternalDiscovered,
+ unknown = unknown,
+ subtitleLanguageChineseCantonese = subtitleLanguageChineseCantonese,
+ subtitleLanguageChineseSimplified = subtitleLanguageChineseSimplified,
+ subtitleLanguageChineseTraditional = subtitleLanguageChineseTraditional,
+ subtitleLanguageJapanese = subtitleLanguageJapanese,
+ subtitleLanguageEnglish = subtitleLanguageEnglish,
+ )
+ }
+}
+
object MediaDetailsRenderer {
@JvmName("renderSubtitleKindNotNull")
- fun renderSubtitleKind(subtitleKind: SubtitleKind): String = renderSubtitleKind(subtitleKind as SubtitleKind?)!!
+ fun renderSubtitleKind(subtitleKind: SubtitleKind, strings: MediaDetailsStrings): String =
+ renderSubtitleKind(subtitleKind as SubtitleKind?, strings)!!
fun renderSubtitleKind(
subtitleKind: SubtitleKind?,
+ strings: MediaDetailsStrings,
): String? {
return when (subtitleKind) {
- SubtitleKind.EMBEDDED -> "内嵌"
- SubtitleKind.CLOSED -> "内封"
- SubtitleKind.EXTERNAL_PROVIDED -> "外挂"
- SubtitleKind.EXTERNAL_DISCOVER -> "未知"
- SubtitleKind.CLOSED_OR_EXTERNAL_DISCOVER -> "内封或未知"
+ SubtitleKind.EMBEDDED -> strings.subtitleKindEmbedded
+ SubtitleKind.CLOSED -> strings.subtitleKindClosed
+ SubtitleKind.EXTERNAL_PROVIDED -> strings.subtitleKindExternalProvided
+ SubtitleKind.EXTERNAL_DISCOVER -> strings.subtitleKindExternalDiscovered
+ SubtitleKind.CLOSED_OR_EXTERNAL_DISCOVER -> strings.subtitleKindClosedOrExternalDiscovered
null -> null
}
}
@@ -29,50 +105,79 @@ object MediaDetailsRenderer {
fun renderSubtitleLanguages(
subtitleKind: SubtitleKind?,
subtitleLanguageIds: List,
+ strings: MediaDetailsStrings,
): String = buildString {
if (subtitleKind != null) {
append("[")
- append(renderSubtitleKind(subtitleKind))
+ append(renderSubtitleKind(subtitleKind, strings))
append("] ")
} else {
if (subtitleLanguageIds.isEmpty()) {
- append("未知")
+ append(strings.unknown)
}
}
for ((index, subtitleLanguageId) in subtitleLanguageIds.withIndex()) {
- append(renderSubtitleLanguage(subtitleLanguageId))
+ append(renderSubtitleLanguage(subtitleLanguageId, strings))
if (index != subtitleLanguageIds.size - 1) {
append(" ")
}
}
}
+
+ @JvmName("renderSubtitleLanguagesTyped")
+ fun renderSubtitleLanguages(
+ subtitleKind: SubtitleKind?,
+ subtitleLanguages: List,
+ strings: MediaDetailsStrings,
+ ): String = buildString {
+ if (subtitleKind != null) {
+ append("[")
+ append(renderSubtitleKind(subtitleKind, strings))
+ append("] ")
+ } else {
+ if (subtitleLanguages.isEmpty()) {
+ append(strings.unknown)
+ }
+ }
+
+ for ((index, subtitleLanguage) in subtitleLanguages.withIndex()) {
+ append(renderSubtitleLanguage(subtitleLanguage, strings))
+ if (index != subtitleLanguages.size - 1) {
+ append(" ")
+ }
+ }
+ }
}
@Stable
-fun Media.renderProperties(): String {
+fun Media.renderProperties(strings: MediaDetailsStrings): String {
val properties = this.properties
return listOfNotNull(
properties.resolution,
- properties.subtitleLanguageIds.joinToString("/") { renderSubtitleLanguage(it) }
+ properties.subtitleLanguageIds.joinToString("/") { renderSubtitleLanguage(it, strings) }
.takeIf { it.isNotBlank() },
properties.size.takeIf { it != 0.bytes && it != Unspecified },
properties.alliance,
).joinToString(" · ")
}
-fun renderSubtitleLanguage(id: String): String {
- return when (id) {
- SubtitleLanguage.ChineseCantonese.id -> "粤语"
- SubtitleLanguage.ChineseSimplified.id -> "简中"
- SubtitleLanguage.ChineseTraditional.id -> "繁中"
- SubtitleLanguage.Japanese.id -> "日语"
- SubtitleLanguage.English.id -> "英语"
- else -> id
+fun renderSubtitleLanguage(id: String, strings: MediaDetailsStrings): String {
+ return SubtitleLanguage.tryParse(id)?.let { renderSubtitleLanguage(it, strings) } ?: id
+}
+
+fun renderSubtitleLanguage(language: SubtitleLanguage, strings: MediaDetailsStrings): String {
+ return when (language) {
+ SubtitleLanguage.ChineseCantonese -> strings.subtitleLanguageChineseCantonese
+ SubtitleLanguage.ChineseSimplified -> strings.subtitleLanguageChineseSimplified
+ SubtitleLanguage.ChineseTraditional -> strings.subtitleLanguageChineseTraditional
+ SubtitleLanguage.Japanese -> strings.subtitleLanguageJapanese
+ SubtitleLanguage.English -> strings.subtitleLanguageEnglish
+ SubtitleLanguage.ParseError -> strings.unknown
+ is SubtitleLanguage.Other -> language.displayName
}
}
fun renderResolution(id: String): String {
return Resolution.tryParse(id)?.displayName ?: id
}
-
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/rating/RatingText.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/rating/RatingText.kt
index f02084d519..197d6c5f57 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/rating/RatingText.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/rating/RatingText.kt
@@ -34,8 +34,11 @@ import me.him188.ani.app.data.models.subject.RatingCounts
import me.him188.ani.app.data.models.subject.RatingInfo
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.rating_summary_multiline
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.format1f
+import org.jetbrains.compose.resources.stringResource
// https://www.figma.com/design/LET1n9mmDa6npDTIlUuJjU/Main?node-id=133-2765&t=innxKfrf4vLdukgs-4
@@ -67,7 +70,7 @@ fun RatingText(
) {
Column {
Text(
- "#${rating.rank}\n${rating.total} 人评",
+ stringResource(Lang.rating_summary_multiline, rating.rank, rating.total),
maxLines = 2,
softWrap = false,
)
@@ -138,4 +141,3 @@ fun PreviewRatingTextIntrinsicMin() {
}
}
}
-
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/richtext/RichText.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/richtext/RichText.kt
index a1d4a7d034..82f265c476 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/richtext/RichText.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/richtext/RichText.kt
@@ -130,15 +130,21 @@ object RichTextDefaults {
val FontSize: Float = 16f
@UiThread
- fun checkSanityAndOpen(url: String, navigator: UriHandler, toaster: Toaster) {
+ fun checkSanityAndOpen(
+ url: String,
+ navigator: UriHandler,
+ toaster: Toaster,
+ externalAppLinkWarningPrefix: String,
+ openLinkFailedPrefix: String,
+ ) {
try {
if (url.startsWith("https://") || url.startsWith("http://")) {
navigator.openUri(url)
} else {
- toaster.toast("此链接可能会打开其他应用,ani 将不会打开此链接:\n$url")
+ toaster.toast(externalAppLinkWarningPrefix + url)
}
} catch (ex: Exception) {
- toaster.toast("无法打开此链接:\n$url")
+ toaster.toast(openLinkFailedPrefix + url)
}
}
@@ -404,4 +410,4 @@ object RichTextDefaults {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/search/LazyListExtensions.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/search/LazyListExtensions.kt
index caae25c89c..2bcd034b3f 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/search/LazyListExtensions.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/search/LazyListExtensions.kt
@@ -25,6 +25,9 @@ import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.layout.minimumHairlineSize
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_search_no_more_items
+import org.jetbrains.compose.resources.stringResource
/**
* 展示一个错误提示卡片, 仅在加载失败时显示. 建议放在 [androidx.compose.foundation.lazy.LazyColumn] 第一个 item.
@@ -64,7 +67,7 @@ fun LazyListScope.noMoreItemsItem(
contentAlignment = Alignment.Center,
) {
Text(
- "没有更多了",
+ stringResource(Lang.foundation_search_no_more_items),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
)
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/search/LoadErrorCard.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/search/LoadErrorCard.kt
index 0bf1e54f1a..eb62909d0d 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/search/LoadErrorCard.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/search/LoadErrorCard.kt
@@ -53,8 +53,22 @@ import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.icons.Passkey_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24
import me.him188.ani.app.ui.foundation.setClipEntryText
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_load_error_copied_feedback
+import me.him188.ani.app.ui.lang.foundation_load_error_network
+import me.him188.ani.app.ui.lang.foundation_load_error_no_details
+import me.him188.ani.app.ui.lang.foundation_load_error_no_results
+import me.him188.ani.app.ui.lang.foundation_load_error_rate_limited
+import me.him188.ani.app.ui.lang.foundation_load_error_request_error
+import me.him188.ani.app.ui.lang.foundation_load_error_requires_login
+import me.him188.ani.app.ui.lang.foundation_load_error_service_unavailable
+import me.him188.ani.app.ui.lang.foundation_load_error_unknown_with_message
+import me.him188.ani.app.ui.lang.login_sign_in
+import me.him188.ani.app.ui.lang.settings_mediasource_copy
+import me.him188.ani.app.ui.lang.settings_mediasource_retry
import me.him188.ani.utils.logging.error
import me.him188.ani.utils.logging.logger
+import org.jetbrains.compose.resources.stringResource
@Composable
@@ -92,6 +106,10 @@ fun LoadErrorCard(
) {
if (error == null) return
val role = LoadErrorCardRole.from(error)
+ val retryText = stringResource(Lang.settings_mediasource_retry)
+ val signInText = stringResource(Lang.login_sign_in)
+ val copyText = stringResource(Lang.settings_mediasource_copy)
+ val copiedFeedbackText = stringResource(Lang.foundation_load_error_copied_feedback)
val retryButton = @Composable {
SearchDefaults.IconTextButton(
@@ -102,7 +120,7 @@ fun LoadErrorCard(
iconModifier,
)
},
- text = { Text("重试") },
+ text = { Text(retryText) },
)
}
@@ -163,7 +181,7 @@ fun LoadErrorCard(
iconModifier,
)
},
- text = { Text("登录") },
+ text = { Text(signInText) },
)
},
colors = listItemColors,
@@ -203,10 +221,10 @@ fun LoadErrorCard(
""
}
}
- toaster.toast("已复制,请反馈到 GitHub issues 或群里")
+ toaster.toast(copiedFeedbackText)
},
) {
- Text("复制")
+ Text(copyText)
}
}
@@ -274,19 +292,23 @@ object LoadErrorDefaults {
get() = MaterialTheme.colorScheme.surfaceContainerHighest
}
+@Composable
fun renderLoadErrorMessage(error: LoadError): String {
return when (error) {
- LoadError.NetworkError -> "网络错误"
- LoadError.RateLimited -> "操作过快,请重试"
- LoadError.ServiceUnavailable -> "服务暂不可用"
- LoadError.NoResults -> "无搜索结果"
- LoadError.RequiresLogin -> "此功能需要登录"
+ LoadError.NetworkError -> stringResource(Lang.foundation_load_error_network)
+ LoadError.RateLimited -> stringResource(Lang.foundation_load_error_rate_limited)
+ LoadError.ServiceUnavailable -> stringResource(Lang.foundation_load_error_service_unavailable)
+ LoadError.NoResults -> stringResource(Lang.foundation_load_error_no_results)
+ LoadError.RequiresLogin -> stringResource(Lang.foundation_load_error_requires_login)
is LoadError.UnknownError -> {
error.throwable?.printStackTrace()
- "未知错误: ${error.throwable?.message ?: "无详细信息"}"
+ stringResource(
+ Lang.foundation_load_error_unknown_with_message,
+ error.throwable?.message ?: stringResource(Lang.foundation_load_error_no_details),
+ )
}
- is LoadError.RequestError -> "请求错误: ${error.localized}"
+ is LoadError.RequestError -> stringResource(Lang.foundation_load_error_request_error, error.localized)
}
}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/AiringLabel.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/AiringLabel.kt
index 39d39fc822..99341f8353 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/AiringLabel.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/AiringLabel.kt
@@ -30,7 +30,6 @@ import me.him188.ani.app.data.models.subject.SubjectAiringInfo
import me.him188.ani.app.data.models.subject.SubjectAiringKind
import me.him188.ani.app.data.models.subject.SubjectProgressInfo
import me.him188.ani.app.data.models.subject.TestSubjectProgressInfos
-import me.him188.ani.app.data.models.subject.computeTotalEpisodeText
import me.him188.ani.app.data.models.subject.isOnAir
import me.him188.ani.app.ui.foundation.stateOf
import me.him188.ani.datasources.api.EpisodeSort
@@ -52,17 +51,17 @@ class AiringLabelState(
/**
* 显示当前看到的剧集, 或者最新连载到的剧集.
*/
- val progressText by derivedStateOf {
+ fun progressText(strings: SubjectStatusStrings): String? {
// Hi, 如果你修改这里, 务必在 AiringLabelStateTest 增加测试
val airingInfo = airingInfo
val progressInfo = progressInfo
if (airingInfo == null || progressInfo == null) {
- return@derivedStateOf null
+ return null
}
- when (airingInfo.kind) {
+ return when (airingInfo.kind) {
SubjectAiringKind.UPCOMING -> {
- "未开播"
+ strings.upcoming
// if (airingInfo.airDate.isInvalid) {
// "未开播"
// } else {
@@ -72,32 +71,32 @@ class AiringLabelState(
SubjectAiringKind.ON_AIR -> {
when (val s = progressInfo.continueWatchingStatus) {
- ContinueWatchingStatus.Done -> "已看完"
- is ContinueWatchingStatus.Watched -> "看过 ${renderEpAndSort(s.episodeEp, s.episodeSort)}"
+ ContinueWatchingStatus.Done -> strings.done
+ is ContinueWatchingStatus.Watched -> strings.watched(renderEpAndSort(s.episodeEp, s.episodeSort))
is ContinueWatchingStatus.Continue,
is ContinueWatchingStatus.NotOnAir,
is ContinueWatchingStatus.Start,
->
if (airingInfo.latestSort == null) {
- "连载中"
+ strings.onAir
} else {
- "连载至 ${renderEpAndSort(airingInfo.latestEp, airingInfo.latestSort)}"
+ strings.onAirTo(renderEpAndSort(airingInfo.latestEp, airingInfo.latestSort))
}
}
}
SubjectAiringKind.COMPLETED -> {
when (val s = progressInfo.continueWatchingStatus) {
- ContinueWatchingStatus.Done -> "已看完"
+ ContinueWatchingStatus.Done -> strings.done
- is ContinueWatchingStatus.Watched -> "看过 ${renderEpAndSort(s.episodeEp, s.episodeSort)}"
+ is ContinueWatchingStatus.Watched -> strings.watched(renderEpAndSort(s.episodeEp, s.episodeSort))
is ContinueWatchingStatus.Continue ->
- "看过 ${renderEpAndSort(s.watchedEpisodeEp, s.watchedEpisodeSort)}"
+ strings.watched(renderEpAndSort(s.watchedEpisodeEp, s.watchedEpisodeSort))
is ContinueWatchingStatus.NotOnAir,
ContinueWatchingStatus.Start,
- -> "已完结"
+ -> strings.completed
}
}
}
@@ -122,9 +121,9 @@ class AiringLabelState(
/**
* "全 xx 话"
*/
- val totalEpisodesText by derivedStateOf {
- val airingInfo = airingInfo ?: return@derivedStateOf null
- airingInfo.computeTotalEpisodeText()
+ fun totalEpisodesText(strings: SubjectStatusStrings): String? {
+ val airingInfo = airingInfo ?: return null
+ return renderTotalEpisodeText(airingInfo, strings)
}
}
@@ -146,19 +145,20 @@ fun AiringLabel(
style: TextStyle = LocalTextStyle.current,
progressColor: Color = if (state.highlightProgress) MaterialTheme.colorScheme.primary else Color.Unspecified,
) {
+ val strings = rememberSubjectStatusStrings()
ProvideTextStyle(style) {
FlowRow(
modifier,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
) {
- state.progressText?.let {
+ state.progressText(strings)?.let {
Text(
it,
color = progressColor,
softWrap = false,
)
}
- state.totalEpisodesText?.let {
+ state.totalEpisodesText(strings)?.let {
Text(" · ", softWrap = false)
Text(it, softWrap = false)
}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectProgressState.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectProgressState.kt
index 8493b9a81d..56849a451d 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectProgressState.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectProgressState.kt
@@ -48,32 +48,32 @@ class SubjectProgressState(
info.value?.nextEpisodeIdToPlay
}
- val buttonText by derivedStateOf {
- when (val s = continueWatchingStatus) {
- is ContinueWatchingStatus.Continue -> "继续观看 ${renderEpAndSort(s.episodeEp, s.episodeSort)}"
- ContinueWatchingStatus.Done -> "已看完"
+ fun buttonText(strings: SubjectStatusStrings): String {
+ return when (val s = continueWatchingStatus) {
+ is ContinueWatchingStatus.Continue -> strings.continueWatching(renderEpAndSort(s.episodeEp, s.episodeSort))
+ ContinueWatchingStatus.Done -> strings.done
is ContinueWatchingStatus.NotOnAir -> {
val date = s.airDate.toLocalDateOrNull()
if (date != null) {
val week = weekFormatter.format(date)
- "${week}开播"
+ strings.startsOn(week)
} else {
- "还未开播"
+ strings.notOnAir
}
}
- ContinueWatchingStatus.Start -> "开始观看"
+ ContinueWatchingStatus.Start -> strings.startWatching
is ContinueWatchingStatus.Watched -> {
val date = s.nextEpisodeAirDate.toLocalDateOrNull()
if (date != null) {
val week = weekFormatter.format(date)
- "${week}更新"
+ strings.updatesOn(week)
} else {
- "看过 ${renderEpAndSort(s.episodeEp, s.episodeSort)}"
+ strings.watched(renderEpAndSort(s.episodeEp, s.episodeSort))
}
}
- null -> "未知"
+ null -> strings.unknown
}
}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectRendering.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectRendering.kt
index 455a7a0144..a27320fc08 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectRendering.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectRendering.kt
@@ -9,16 +9,29 @@
package me.him188.ani.app.ui.subject
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_rendering_season_year_month
import me.him188.ani.datasources.api.PackedDate
import me.him188.ani.datasources.api.seasonMonth
-
+import org.jetbrains.compose.resources.getString
+import org.jetbrains.compose.resources.stringResource
@Stable
+@Composable
fun renderSubjectSeason(date: PackedDate): String {
if (date == PackedDate.Invalid) return "TBA"
if (date.seasonMonth == 0) {
return date.toString()
}
- return "${date.year} 年 ${date.seasonMonth} 月"
+ return stringResource(Lang.subject_rendering_season_year_month, date.year.toString(), date.seasonMonth.toString())
+}
+
+suspend fun getSubjectSeasonText(date: PackedDate): String {
+ if (date == PackedDate.Invalid) return "TBA"
+ if (date.seasonMonth == 0) {
+ return date.toString()
+ }
+ return getString(Lang.subject_rendering_season_year_month, date.year.toString(), date.seasonMonth.toString())
}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectStatusStrings.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectStatusStrings.kt
new file mode 100644
index 0000000000..c8d0254a7c
--- /dev/null
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/subject/SubjectStatusStrings.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024-2026 OpenAni and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
+ *
+ * https://github.com/open-ani/ani/blob/main/LICENSE
+ */
+
+package me.him188.ani.app.ui.subject
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_airing_completed
+import me.him188.ani.app.ui.lang.subject_airing_on_air
+import me.him188.ani.app.ui.lang.subject_airing_on_air_to
+import me.him188.ani.app.ui.lang.subject_airing_total_episodes_completed
+import me.him188.ani.app.ui.lang.subject_airing_total_episodes_scheduled
+import me.him188.ani.app.ui.lang.subject_airing_upcoming
+import me.him188.ani.app.ui.lang.subject_progress_continue_watching
+import me.him188.ani.app.ui.lang.subject_progress_done
+import me.him188.ani.app.ui.lang.subject_progress_not_on_air
+import me.him188.ani.app.ui.lang.subject_progress_start_watching
+import me.him188.ani.app.ui.lang.subject_progress_starts_on
+import me.him188.ani.app.ui.lang.subject_progress_unknown
+import me.him188.ani.app.ui.lang.subject_progress_updates_on
+import me.him188.ani.app.ui.lang.subject_progress_watched
+import org.jetbrains.compose.resources.stringResource
+
+@Stable
+data class SubjectStatusStrings(
+ val continueWatchingFormat: String,
+ val done: String,
+ val notOnAir: String,
+ val startsOnFormat: String,
+ val startWatching: String,
+ val updatesOnFormat: String,
+ val watchedFormat: String,
+ val unknown: String,
+ val upcoming: String,
+ val onAir: String,
+ val onAirToFormat: String,
+ val completed: String,
+ val totalEpisodesCompletedFormat: String,
+ val totalEpisodesScheduledFormat: String,
+) {
+ fun continueWatching(episode: String): String = continueWatchingFormat.replacePlaceholder(episode)
+ fun startsOn(whenText: String): String = startsOnFormat.replacePlaceholder(whenText)
+ fun updatesOn(whenText: String): String = updatesOnFormat.replacePlaceholder(whenText)
+ fun watched(episode: String): String = watchedFormat.replacePlaceholder(episode)
+ fun onAirTo(episode: String): String = onAirToFormat.replacePlaceholder(episode)
+ fun totalEpisodesCompleted(count: Int): String = totalEpisodesCompletedFormat.replacePlaceholder(count.toString())
+ fun totalEpisodesScheduled(count: Int): String = totalEpisodesScheduledFormat.replacePlaceholder(count.toString())
+}
+
+@Composable
+fun rememberSubjectStatusStrings(): SubjectStatusStrings = SubjectStatusStrings(
+ continueWatchingFormat = stringResource(Lang.subject_progress_continue_watching),
+ done = stringResource(Lang.subject_progress_done),
+ notOnAir = stringResource(Lang.subject_progress_not_on_air),
+ startsOnFormat = stringResource(Lang.subject_progress_starts_on),
+ startWatching = stringResource(Lang.subject_progress_start_watching),
+ updatesOnFormat = stringResource(Lang.subject_progress_updates_on),
+ watchedFormat = stringResource(Lang.subject_progress_watched),
+ unknown = stringResource(Lang.subject_progress_unknown),
+ upcoming = stringResource(Lang.subject_airing_upcoming),
+ onAir = stringResource(Lang.subject_airing_on_air),
+ onAirToFormat = stringResource(Lang.subject_airing_on_air_to),
+ completed = stringResource(Lang.subject_airing_completed),
+ totalEpisodesCompletedFormat = stringResource(Lang.subject_airing_total_episodes_completed),
+ totalEpisodesScheduledFormat = stringResource(Lang.subject_airing_total_episodes_scheduled),
+)
+
+fun renderTotalEpisodeText(
+ airingInfo: me.him188.ani.app.data.models.subject.SubjectAiringInfo,
+ strings: SubjectStatusStrings,
+): String? {
+ return if (
+ airingInfo.kind == me.him188.ani.app.data.models.subject.SubjectAiringKind.UPCOMING &&
+ airingInfo.mainEpisodeCount == 0
+ ) {
+ null
+ } else {
+ when (airingInfo.kind) {
+ me.him188.ani.app.data.models.subject.SubjectAiringKind.COMPLETED ->
+ strings.totalEpisodesCompleted(airingInfo.mainEpisodeCount)
+
+ me.him188.ani.app.data.models.subject.SubjectAiringKind.UPCOMING,
+ me.him188.ani.app.data.models.subject.SubjectAiringKind.ON_AIR,
+ ->
+ strings.totalEpisodesScheduled(airingInfo.mainEpisodeCount)
+ }
+ }
+}
+
+private fun String.replacePlaceholder(value: String): String {
+ return replace("%1\$s", value).replace("%s", value)
+}
diff --git a/app/shared/ui-foundation/src/commonMain/kotlin/ui/user/BangumiFullSyncStateDialog.kt b/app/shared/ui-foundation/src/commonMain/kotlin/ui/user/BangumiFullSyncStateDialog.kt
index 14064117ed..993b70c5e3 100644
--- a/app/shared/ui-foundation/src/commonMain/kotlin/ui/user/BangumiFullSyncStateDialog.kt
+++ b/app/shared/ui-foundation/src/commonMain/kotlin/ui/user/BangumiFullSyncStateDialog.kt
@@ -24,6 +24,20 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import me.him188.ani.app.data.models.bangumi.BangumiSyncState
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_continue_background
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_description
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_failed
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_fetching_episodes
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_fetching_metadata
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_fetching_subjects
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_finishing
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_in_progress
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_inserting
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_preparing
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_success
+import me.him188.ani.app.ui.lang.foundation_bangumi_sync_title
+import org.jetbrains.compose.resources.stringResource
@Composable
fun BangumiFullSyncStateDialog(
@@ -31,7 +45,7 @@ fun BangumiFullSyncStateDialog(
onDismissRequest: () -> Unit,
) {
AlertDialog(
- title = { Text("正在下载 Bangumi 收藏数据") },
+ title = { Text(stringResource(Lang.foundation_bangumi_sync_title)) },
text = {
Column {
Text(renderBangumiSyncState(state))
@@ -42,36 +56,56 @@ fun BangumiFullSyncStateDialog(
LinearProgressIndicator({ 1f }, modifier = Modifier.fillMaxWidth())
}
Spacer(modifier = Modifier.height(24.dp))
- Text("此操作可能需要 5-15 分钟时间,请耐心等待。在下载过程中,你可以正常使用其他功能。可手动刷新收藏列表查看最新进度。")
+ Text(stringResource(Lang.foundation_bangumi_sync_description))
}
},
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onDismissRequest) {
- Text("在后台继续")
+ Text(stringResource(Lang.foundation_bangumi_sync_continue_background))
}
},
properties = DialogProperties(dismissOnClickOutside = false),
)
}
+@Composable
private fun renderBangumiSyncState(state: BangumiSyncState?): String {
return when (state) {
- null -> "准备中"
- BangumiSyncState.Preparing -> "正在获取元数据"
- is BangumiSyncState.FetchingSubjects -> "(已完成 ${state.fetchedCount} 条) 正在获取更多收藏列表"
- is BangumiSyncState.FetchingEpisodes -> "(已完成 ${state.fetchedCount} 条) 正在获取观看进度"
- is BangumiSyncState.Inserting -> "(已完成 ${state.savedCount} 条) 正在保存"
- is BangumiSyncState.Finishing -> "(已完成 ${state.savedCount} 条) 正在完成"
+ null -> stringResource(Lang.foundation_bangumi_sync_preparing)
+ BangumiSyncState.Preparing -> stringResource(Lang.foundation_bangumi_sync_fetching_metadata)
+ is BangumiSyncState.FetchingSubjects -> stringResource(
+ Lang.foundation_bangumi_sync_fetching_subjects,
+ state.fetchedCount,
+ )
+
+ is BangumiSyncState.FetchingEpisodes -> stringResource(
+ Lang.foundation_bangumi_sync_fetching_episodes,
+ state.fetchedCount,
+ )
+
+ is BangumiSyncState.Inserting -> stringResource(
+ Lang.foundation_bangumi_sync_inserting,
+ state.savedCount,
+ )
+
+ is BangumiSyncState.Finishing -> stringResource(
+ Lang.foundation_bangumi_sync_finishing,
+ state.savedCount,
+ )
is BangumiSyncState.Finished -> {
if (state.error != null) {
- "(已完成 ${state.savedCount} 条) 同步失败, 错误信息如下: \n$state"
+ stringResource(
+ Lang.foundation_bangumi_sync_failed,
+ state.savedCount,
+ state.toString(),
+ )
} else {
- "(已完成 ${state.savedCount} 条) 同步成功"
+ stringResource(Lang.foundation_bangumi_sync_success, state.savedCount)
}
}
- BangumiSyncState.Unsupported -> "进行中"
+ BangumiSyncState.Unsupported -> stringResource(Lang.foundation_bangumi_sync_in_progress)
}
}
@@ -96,4 +130,4 @@ private fun PreviewBangumiFullSyncDialogSyncTimeline() {
onDismissRequest = {},
)
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorFilters.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorFilters.kt
index c17e25a3cd..40c844448b 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorFilters.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorFilters.kt
@@ -46,7 +46,16 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import me.him188.ani.app.ui.foundation.dialogs.PlatformPopupProperties
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.media_selector_filter_alliance
+import me.him188.ani.app.ui.lang.media_selector_filter_clear
+import me.him188.ani.app.ui.lang.media_selector_filter_expand
+import me.him188.ani.app.ui.lang.media_selector_filter_resolution
+import me.him188.ani.app.ui.lang.media_selector_filter_selected
+import me.him188.ani.app.ui.lang.media_selector_filter_subtitle
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.media.renderSubtitleLanguage
+import org.jetbrains.compose.resources.stringResource
private inline val minWidth get() = 60.dp
@@ -64,6 +73,10 @@ fun MediaSelectorFilters(
singleLine: Boolean = false,
) {
val scope = rememberCoroutineScope()
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
+ val resolutionText = stringResource(Lang.media_selector_filter_resolution)
+ val subtitleText = stringResource(Lang.media_selector_filter_subtitle)
+ val allianceText = stringResource(Lang.media_selector_filter_alliance)
val content = @Composable {
val resolutionPresentation by resolution.presentationFlow.collectAsStateWithLifecycle()
MediaSelectorFilterChip(
@@ -71,7 +84,7 @@ fun MediaSelectorFilters(
allValues = { resolutionPresentation.available },
onSelect = { scope.launch { resolution.prefer(it) } },
onDeselect = { scope.launch { resolution.removePreference() } },
- name = { Text("分辨率") },
+ name = { Text(resolutionText) },
Modifier.widthIn(min = minWidth, max = maxWidth),
)
val subtitleLanguagePresentation by subtitleLanguageId.presentationFlow.collectAsStateWithLifecycle()
@@ -80,9 +93,9 @@ fun MediaSelectorFilters(
allValues = { subtitleLanguagePresentation.available },
onSelect = { scope.launch { subtitleLanguageId.prefer(it) } },
onDeselect = { scope.launch { subtitleLanguageId.removePreference() } },
- name = { Text("字幕") },
+ name = { Text(subtitleText) },
Modifier.widthIn(min = minWidth, max = maxWidth),
- label = { MediaSelectorFilterChipText(renderSubtitleLanguage(it)) },
+ label = { MediaSelectorFilterChipText(renderSubtitleLanguage(it, mediaDetailsStrings)) },
)
val alliancePresentation by alliance.presentationFlow.collectAsStateWithLifecycle()
MediaSelectorFilterChip(
@@ -90,7 +103,7 @@ fun MediaSelectorFilters(
allValues = { alliancePresentation.available },
onSelect = { scope.launch { alliance.prefer(it) } },
onDeselect = { scope.launch { alliance.removePreference() } },
- name = { Text("字幕组") },
+ name = { Text(allianceText) },
Modifier.widthIn(min = minWidth, max = maxWidth),
)
}
@@ -139,6 +152,9 @@ private fun MediaSelectorFilterChip(
var showDropdown by rememberSaveable {
mutableStateOf(false)
}
+ val expandText = stringResource(Lang.media_selector_filter_expand)
+ val clearText = stringResource(Lang.media_selector_filter_clear)
+ val selectedText = stringResource(Lang.media_selector_filter_selected)
val allValuesState by remember(allValues) {
derivedStateOf(allValues)
@@ -177,10 +193,10 @@ private fun MediaSelectorFilterChip(
trailingIcon = if (isSingleValue) null else {
{
if (selected == null) {
- Icon(Icons.Default.ArrowDropDown, "展开")
+ Icon(Icons.Default.ArrowDropDown, expandText)
} else {
Icon(
- Icons.Default.Close, "取消筛选",
+ Icons.Default.Close, clearText,
Modifier.clickable { selectedState?.let { onDeselect(it) } },
)
}
@@ -204,7 +220,7 @@ private fun MediaSelectorFilterChip(
text = { label(item) },
trailingIcon = {
if (selectedState == item) {
- Icon(Icons.Default.Check, "当前选中")
+ Icon(Icons.Default.Check, selectedText)
}
},
onClick = {
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorHelp.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorHelp.kt
index 25481076d7..43a1431e64 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorHelp.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorHelp.kt
@@ -28,7 +28,15 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.widgets.RichDialogLayout
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_details_source_online
+import me.him188.ani.app.ui.lang.media_selector_help_bt_description
+import me.him188.ani.app.ui.lang.media_selector_help_source_types
+import me.him188.ani.app.ui.lang.media_selector_help_title
+import me.him188.ani.app.ui.lang.media_selector_help_web_description
+import me.him188.ani.app.ui.lang.subject_episode_close
import me.him188.ani.app.ui.settings.rendering.MediaSourceIcons
+import org.jetbrains.compose.resources.stringResource
@Composable
@@ -36,16 +44,22 @@ fun MediaSelectorHelp(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier
) {
+ val titleText = stringResource(Lang.media_selector_help_title)
+ val closeText = stringResource(Lang.subject_episode_close)
+ val sourceTypesText = stringResource(Lang.media_selector_help_source_types)
+ val btDescriptionText = stringResource(Lang.media_selector_help_bt_description)
+ val onlineText = stringResource(Lang.cache_details_source_online)
+ val webDescriptionText = stringResource(Lang.media_selector_help_web_description)
RichDialogLayout(
- title = { Text("数据源帮助") },
+ title = { Text(titleText) },
buttons = {
TextButton(onDismissRequest) {
- Text("关闭")
+ Text(closeText)
}
},
modifier,
) {
- Text("数据源类型", style = MaterialTheme.typography.titleMedium)
+ Text(sourceTypesText, style = MaterialTheme.typography.titleMedium)
Row(Modifier.padding(top = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ExplainerCard(
@@ -55,16 +69,16 @@ fun MediaSelectorHelp(
Icon(MediaSourceIcons.KindBT, null)
},
) {
- Text("从 BitTorrent 网络获取资源,清晰度高,资源全面,加载速度可能不快")
+ Text(btDescriptionText)
}
ExplainerCard(
- title = { Text("在线") },
+ title = { Text(onlineText) },
Modifier.weight(1f),
icon = {
Icon(MediaSourceIcons.KindWeb, null)
},
) {
- Text("从在线视频网站获取资源,加载速度快,但清晰度通常不高")
+ Text(webDescriptionText)
}
}
}
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorItem.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorItem.kt
index 85a40d9241..5b62e411ae 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorItem.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorItem.kt
@@ -57,7 +57,14 @@ import me.him188.ani.app.tools.formatDateTime
import me.him188.ani.app.ui.foundation.setClipEntryText
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_unknown
+import me.him188.ani.app.ui.lang.media_selector_item_no_subtitle
+import me.him188.ani.app.ui.lang.media_selector_item_season_mismatch
+import me.him188.ani.app.ui.lang.media_selector_item_single_episode_resource
+import me.him188.ani.app.ui.lang.media_selector_item_subject_title_mismatch
+import me.him188.ani.app.ui.lang.media_selector_item_unsupported_playback
import me.him188.ani.app.ui.lang.settings_debug_copied
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.media.renderSubtitleLanguage
import me.him188.ani.app.ui.settings.rendering.MediaSourceIcon
import me.him188.ani.app.ui.settings.rendering.MediaSourceIcons
@@ -65,6 +72,7 @@ import me.him188.ani.datasources.api.Media
import me.him188.ani.datasources.api.topic.FileSize
import me.him188.ani.datasources.api.topic.ResourceLocation
import org.jetbrains.compose.resources.getString
+import org.jetbrains.compose.resources.stringResource
@OptIn(UnsafeOriginalMediaAccess::class)
@@ -83,9 +91,15 @@ internal fun MediaSelectorItem(
) {
// We use the first media for display because the group has the same info.
val media: Media = group.first.original
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
val clipboard = LocalClipboard.current
val toaster = LocalToaster.current
val scope = rememberCoroutineScope()
+ val noSubtitleText = stringResource(Lang.media_selector_item_no_subtitle)
+ val singleEpisodeResourceText = stringResource(Lang.media_selector_item_single_episode_resource)
+ val unsupportedPlaybackText = stringResource(Lang.media_selector_item_unsupported_playback)
+ val seasonMismatchText = stringResource(Lang.media_selector_item_season_mismatch)
+ val subjectTitleMismatchText = stringResource(Lang.media_selector_item_subject_title_mismatch)
// Determine the reason text, if any
val reasonText = group.exclusionReason?.let { reason ->
@@ -93,12 +107,12 @@ internal fun MediaSelectorItem(
reason.toString()
} else {
when (reason) {
- MediaExclusionReason.MediaWithoutSubtitle -> "无字幕"
- is MediaExclusionReason.SingleEpisodeForCompleteSubject -> "单集资源"
- MediaExclusionReason.UnsupportedByPlatformPlayer -> "不支持播放"
- MediaExclusionReason.FromSequelSeason -> "季度不匹配"
- MediaExclusionReason.FromSeriesSeason -> "季度不匹配(2)"
- MediaExclusionReason.SubjectNameMismatch -> "条目标题不匹配"
+ MediaExclusionReason.MediaWithoutSubtitle -> noSubtitleText
+ is MediaExclusionReason.SingleEpisodeForCompleteSubject -> singleEpisodeResourceText
+ MediaExclusionReason.UnsupportedByPlatformPlayer -> unsupportedPlaybackText
+ MediaExclusionReason.FromSequelSeason -> seasonMismatchText
+ MediaExclusionReason.FromSeriesSeason -> seasonMismatchText
+ MediaExclusionReason.SubjectNameMismatch -> subjectTitleMismatchText
}
}
}
@@ -139,7 +153,7 @@ internal fun MediaSelectorItem(
InputChip(
selected = false,
onClick = { onPreferSubtitleLanguageId(languageId) },
- label = { Text(renderSubtitleLanguage(languageId)) },
+ label = { Text(renderSubtitleLanguage(languageId, mediaDetailsStrings)) },
enabled = preferredSubtitleLanguageId() != languageId,
)
}
@@ -288,11 +302,12 @@ private fun ExposedMediaSourceMenu(
modifier: Modifier = Modifier,
) {
var showMenu by rememberSaveable { mutableStateOf(false) }
+ val unknownText = stringResource(Lang.cache_unknown)
ExposedDropdownMenuBox(showMenu, { showMenu = it }, modifier) {
val currentItem = groupState.selectedItem ?: group.first.original
val currentSourceInfo by mediaSourceInfoProvider.rememberMediaSourceInfo(currentItem.mediaSourceId)
TextField(
- value = currentSourceInfo?.displayName ?: "未知",
+ value = currentSourceInfo?.displayName ?: unknownText,
onValueChange = {},
Modifier
.widthIn(min = 48.dp) // override default
@@ -324,7 +339,7 @@ private fun ExposedMediaSourceMenu(
val item = maybeExcluded.original
val sourceInfo by mediaSourceInfoProvider.rememberMediaSourceInfo(item.mediaSourceId)
DropdownMenuItem(
- text = { Text(sourceInfo?.displayName ?: "未知") },
+ text = { Text(sourceInfo?.displayName ?: unknownText) },
leadingIcon = { MediaSourceIcon(sourceInfo, Modifier.size(24.dp)) },
onClick = {
groupState.selectedItem = item
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorView.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorView.kt
index 270d83c698..a55d4634a0 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorView.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSelectorView.kt
@@ -84,6 +84,10 @@ import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.icons.EditSquare
import me.him188.ani.app.ui.foundation.ifThen
import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.media_selector_view_detailed_mode
+import me.him188.ani.app.ui.lang.media_selector_view_filtered_count
+import me.him188.ani.app.ui.lang.media_selector_view_show_excluded
+import me.him188.ani.app.ui.lang.media_selector_view_simple_mode
import me.him188.ani.app.ui.lang.settings_media_source_more
import me.him188.ani.app.ui.mediafetch.request.MediaFetchRequestEditorDialog
import me.him188.ani.app.ui.mediafetch.request.TestMediaFetchRequest
@@ -227,6 +231,8 @@ private fun ViewKindAndMoreRow(
onRequestFetchRequestEdit: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val simpleModeText = stringResource(Lang.media_selector_view_simple_mode)
+ val detailedModeText = stringResource(Lang.media_selector_view_detailed_mode)
Row(
modifier,
verticalAlignment = Alignment.CenterVertically,
@@ -239,14 +245,14 @@ private fun ViewKindAndMoreRow(
onClick = { onViewKindChange(ViewKind.WEB) },
selected = viewKind == ViewKind.WEB,
) {
- Text("简单模式", softWrap = false)
+ Text(simpleModeText, softWrap = false)
}
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
onClick = { onViewKindChange(ViewKind.BT) },
selected = viewKind == ViewKind.BT,
) {
- Text("详细模式", softWrap = false)
+ Text(detailedModeText, softWrap = false)
}
}
@@ -284,6 +290,11 @@ private fun LegacyBTSourceColumn(
onShowExcludedChange: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val filteredCountText = stringResource(
+ Lang.media_selector_view_filtered_count,
+ presentation.preferredCandidates.size,
+ presentation.filteredCandidates.size,
+ )
LazyColumn(
modifier,
lazyListState,
@@ -321,9 +332,7 @@ private fun LegacyBTSourceColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
- remember(presentation.preferredCandidates.size, presentation.filteredCandidates.size) {
- "筛选到 ${presentation.preferredCandidates.size}/${presentation.filteredCandidates.size} 条资源"
- },
+ filteredCountText,
style = MaterialTheme.typography.titleMedium,
)
@@ -345,13 +354,17 @@ private fun LegacyBTSourceColumn(
if (presentation.groupedMediaListExcluded.isNotEmpty()) {
item {
+ val showExcludedText = stringResource(
+ Lang.media_selector_view_show_excluded,
+ presentation.groupedMediaListExcluded.size,
+ )
Row(
Modifier.fillMaxWidth().padding(vertical = 8.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- "显示已被排除的资源 (${presentation.groupedMediaListExcluded.size})",
+ showExcludedText,
Modifier.padding(end = 8.dp),
style = MaterialTheme.typography.labelLarge,
)
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSourceResultsView.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSourceResultsView.kt
index 376d0022ee..d340a89d91 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSourceResultsView.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/MediaSourceResultsView.kt
@@ -64,10 +64,26 @@ import kotlinx.coroutines.launch
import me.him188.ani.app.navigation.LocalNavigator
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.ifThen
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.cache_details_source_online
+import me.him188.ani.app.ui.lang.media_source_results_captcha_required
+import me.him188.ani.app.ui.lang.media_source_results_click_retry
+import me.him188.ani.app.ui.lang.media_source_results_click_verify
+import me.him188.ani.app.ui.lang.media_source_results_data_sources_count
+import me.him188.ani.app.ui.lang.media_source_results_failed
+import me.him188.ani.app.ui.lang.media_source_results_help
+import me.him188.ani.app.ui.lang.media_source_results_searched
+import me.him188.ani.app.ui.lang.media_source_results_searching
+import me.him188.ani.app.ui.lang.media_source_results_settings
+import me.him188.ani.app.ui.lang.media_source_results_success
+import me.him188.ani.app.ui.lang.media_source_results_temp_enable
+import me.him188.ani.app.ui.lang.media_source_results_verify
+import me.him188.ani.app.ui.lang.settings_mediasource_refresh
import me.him188.ani.app.ui.settings.SettingsTab
import me.him188.ani.app.ui.settings.rendering.MediaSourceIcons
import me.him188.ani.app.ui.settings.rendering.SmallMediaSourceIcon
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
@Composable
fun MediaSourceResultsView(
@@ -111,6 +127,18 @@ fun MediaSourceResultsView(
onRestartSource: (instanceId: String) -> Unit,
modifier: Modifier = Modifier,
) {
+ val searchingText = stringResource(Lang.media_source_results_searching)
+ val searchedText = stringResource(Lang.media_source_results_searched)
+ val dataSourcesCountText = stringResource(
+ Lang.media_source_results_data_sources_count,
+ if (sourceResults.anyLoading) searchingText else searchedText,
+ sourceResults.enabledSourceCount,
+ sourceResults.totalSourceCount,
+ )
+ val refreshText = stringResource(Lang.settings_mediasource_refresh)
+ val helpText = stringResource(Lang.media_source_results_help)
+ val settingsText = stringResource(Lang.media_source_results_settings)
+ val onlineText = stringResource(Lang.cache_details_source_online)
Column(modifier) {
var isShowDetails by rememberSaveable { mutableStateOf(false) }
Row(
@@ -121,14 +149,7 @@ fun MediaSourceResultsView(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- remember(
- sourceResults.anyLoading,
- sourceResults.enabledSourceCount,
- sourceResults.totalSourceCount,
- ) {
- val status = if (sourceResults.anyLoading) "正在查询" else "已查询"
- "$status ${sourceResults.enabledSourceCount}/${sourceResults.totalSourceCount} 数据源"
- },
+ dataSourcesCountText,
Modifier.weight(1f),
style = MaterialTheme.typography.titleMedium,
)
@@ -140,14 +161,14 @@ fun MediaSourceResultsView(
}
}
IconButton(onRefresh) {
- Icon(Icons.Outlined.Refresh, "刷新")
+ Icon(Icons.Outlined.Refresh, refreshText)
}
IconButton({ showHelp = true }) {
- Icon(Icons.AutoMirrored.Outlined.HelpOutline, "帮助")
+ Icon(Icons.AutoMirrored.Outlined.HelpOutline, helpText)
}
val navigator = LocalNavigator.current
IconButton({ navigator.navigateSettings(SettingsTab.MEDIA_SOURCE) }) {
- Icon(Icons.Outlined.Settings, "设置")
+ Icon(Icons.Outlined.Settings, settingsText)
}
// TODO: 允许展开的话可能要考虑需要把下面 FlowList 变成 Grid
@@ -190,7 +211,7 @@ fun MediaSourceResultsView(
Icon(MediaSourceIcons.KindBT, null)
ProvideTextStyle(MaterialTheme.typography.labelSmall) {
Box(Modifier.padding(top = 2.dp), contentAlignment = Alignment.Center) {
- Text("在线", Modifier.alpha(0f)) // 相同宽度
+ Text(onlineText, Modifier.alpha(0f)) // 相同宽度
Text("BT")
}
}
@@ -209,7 +230,7 @@ fun MediaSourceResultsView(
Icon(MediaSourceIcons.KindWeb, null)
ProvideTextStyle(MaterialTheme.typography.labelSmall) {
Box(Modifier.padding(top = 2.dp), contentAlignment = Alignment.Center) {
- Text("在线")
+ Text(onlineText)
}
}
}
@@ -289,6 +310,13 @@ private fun MediaSourceResultCard(
modifier: Modifier = Modifier,
preferredSourceContainerColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
) {
+ val temporaryEnableText = stringResource(Lang.media_source_results_temp_enable)
+ val failedText = stringResource(Lang.media_source_results_failed)
+ val clickRetryText = stringResource(Lang.media_source_results_click_retry)
+ val captchaRequiredText = stringResource(Lang.media_source_results_captcha_required)
+ val clickVerifyText = stringResource(Lang.media_source_results_click_verify)
+ val successText = stringResource(Lang.media_source_results_success)
+ val verifyText = stringResource(Lang.media_source_results_verify)
if (expanded) {
OutlinedCard(
onClick = onClick,
@@ -330,7 +358,7 @@ private fun MediaSourceResultCard(
when {
source.isDisabled -> {
Icon(Icons.Outlined.HorizontalRule, null)
- Text("点击临时启用")
+ Text(temporaryEnableText)
}
source.isWorking -> {
@@ -340,20 +368,20 @@ private fun MediaSourceResultCard(
source.isFailedOrAbandoned -> {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
- Icon(Icons.Outlined.Close, "查询失败")
- Text("点击重试")
+ Icon(Icons.Outlined.Close, failedText)
+ Text(clickRetryText)
}
}
source.isCaptchaRequired -> {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
- Icon(Icons.AutoMirrored.Outlined.HelpOutline, "需要验证码")
- Text("点击验证")
+ Icon(Icons.AutoMirrored.Outlined.HelpOutline, captchaRequiredText)
+ Text(clickVerifyText)
}
}
else -> {
- Icon(Icons.Outlined.Check, "查询成功")
+ Icon(Icons.Outlined.Check, successText)
Text(remember(source.totalCount) { "${source.totalCount}" })
}
}
@@ -385,13 +413,13 @@ private fun MediaSourceResultCard(
source.isFailedOrAbandoned -> {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
- Icon(Icons.Outlined.Close, "查询失败")
+ Icon(Icons.Outlined.Close, failedText)
}
}
source.isCaptchaRequired -> {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
- Text("验证")
+ Text(verifyText)
}
}
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/request/MediaFetchRequestEditor.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/request/MediaFetchRequestEditor.kt
index 0d9bd0b018..4372797469 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/request/MediaFetchRequestEditor.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/request/MediaFetchRequestEditor.kt
@@ -48,7 +48,23 @@ import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.foundation.IconButton
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.animation.AniAnimatedVisibility
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_add_name
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_collapse
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_delete_name
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_episode_ep
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_episode_ep_supporting
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_episode_info
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_episode_info_supporting
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_episode_sort
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_episode_sort_supporting
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_expand
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_primary_name
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_primary_name_supporting
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_secondary_names
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_secondary_names_supporting
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
/**
@@ -65,6 +81,19 @@ fun MediaFetchRequestEditor(
modifier: Modifier = Modifier,
) {
val scrollState = rememberScrollState()
+ val primaryNameText = stringResource(Lang.mediafetch_request_editor_primary_name)
+ val primaryNameSupportingText = stringResource(Lang.mediafetch_request_editor_primary_name_supporting)
+ val secondaryNamesText = stringResource(Lang.mediafetch_request_editor_secondary_names)
+ val secondaryNamesSupportingText = stringResource(Lang.mediafetch_request_editor_secondary_names_supporting)
+ val collapseText = stringResource(Lang.mediafetch_request_editor_collapse)
+ val expandText = stringResource(Lang.mediafetch_request_editor_expand)
+ val addNameText = stringResource(Lang.mediafetch_request_editor_add_name)
+ val episodeInfoText = stringResource(Lang.mediafetch_request_editor_episode_info)
+ val episodeInfoSupportingText = stringResource(Lang.mediafetch_request_editor_episode_info_supporting)
+ val episodeSortText = stringResource(Lang.mediafetch_request_editor_episode_sort)
+ val episodeSortSupportingText = stringResource(Lang.mediafetch_request_editor_episode_sort_supporting)
+ val episodeEpText = stringResource(Lang.mediafetch_request_editor_episode_ep)
+ val episodeEpSupportingText = stringResource(Lang.mediafetch_request_editor_episode_ep_supporting)
val listItemColors = ListItemDefaults.colors(
containerColor = Color.Transparent,
@@ -98,8 +127,8 @@ fun MediaFetchRequestEditor(
OutlinedTextField(
value = fetchRequest.primaryName,
onValueChange = { onFetchRequestChange(fetchRequest.copy(primaryName = it)) },
- label = { Text("主搜索名") },
- supportingText = { Text("大多数数据源只使用此名称") },
+ label = { Text(primaryNameText) },
+ supportingText = { Text(primaryNameSupportingText) },
modifier = Modifier.fillMaxWidth().padding(horizontal = horizontalPadding),
singleLine = true,
)
@@ -110,14 +139,14 @@ fun MediaFetchRequestEditor(
ListItem(
headlineContent = {
Text(
- "次要搜索名",
+ secondaryNamesText,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
},
Modifier.padding(top = 8.dp),
supportingContent = {
- Text("在线源会忽略这些名称")
+ Text(secondaryNamesSupportingText)
},
colors = listItemColors,
trailingContent = {
@@ -127,9 +156,9 @@ fun MediaFetchRequestEditor(
) {
// expand/collapse
if (showComplementaryNames) {
- Icon(Icons.Default.KeyboardArrowUp, contentDescription = "收起")
+ Icon(Icons.Default.KeyboardArrowUp, contentDescription = collapseText)
} else {
- Icon(Icons.Default.KeyboardArrowDown, contentDescription = "展开")
+ Icon(Icons.Default.KeyboardArrowDown, contentDescription = expandText)
}
}
},
@@ -161,7 +190,13 @@ fun MediaFetchRequestEditor(
},
Modifier.padding(end = horizontalPadding - 8.dp),
) {
- Icon(Icons.Default.Delete, contentDescription = "删除名称 #$index")
+ Icon(
+ Icons.Default.Delete,
+ contentDescription = stringResource(
+ Lang.mediafetch_request_editor_delete_name,
+ index + 1,
+ ),
+ )
}
}
}
@@ -183,7 +218,7 @@ fun MediaFetchRequestEditor(
Modifier.size(ButtonDefaults.IconSize),
)
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
- Text("添加名称")
+ Text(addNameText)
}
}
}
@@ -193,7 +228,7 @@ fun MediaFetchRequestEditor(
ListItem(
headlineContent = {
Text(
- "剧集信息",
+ episodeInfoText,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
@@ -201,7 +236,7 @@ fun MediaFetchRequestEditor(
Modifier.padding(vertical = 8.dp),
supportingContent = {
Text(
- "资源必须至少匹配以下两种信息中的一种,否则不会显示。可以只修改其中一种",
+ episodeInfoSupportingText,
)
},
colors = listItemColors,
@@ -227,8 +262,8 @@ fun MediaFetchRequestEditor(
onValueChange = { newValue ->
onFetchRequestChange(fetchRequest.copy(episodeSort = newValue))
},
- label = { Text("系列内剧集序号") },
- supportingText = { Text("假设有两季,分别有 12 集,则第二季的第一集为 13") },
+ label = { Text(episodeSortText) },
+ supportingText = { Text(episodeSortSupportingText) },
modifier = Modifier.fillMaxWidth().padding(horizontal = horizontalPadding),
singleLine = true,
isError = sortAndEpAreError,
@@ -238,8 +273,8 @@ fun MediaFetchRequestEditor(
onValueChange = { newValue ->
onFetchRequestChange(fetchRequest.copy(episodeEp = newValue))
},
- label = { Text("条目内序号") },
- supportingText = { Text("在当前季度内的序号,例如第二季的第一集为 01") },
+ label = { Text(episodeEpText) },
+ supportingText = { Text(episodeEpSupportingText) },
modifier = Modifier.fillMaxWidth().padding(horizontal = horizontalPadding),
singleLine = true,
isError = sortAndEpAreError,
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/request/MediaFetchRequestEditorDialog.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/request/MediaFetchRequestEditorDialog.kt
index d9d140f789..eaedccd129 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/request/MediaFetchRequestEditorDialog.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediafetch/request/MediaFetchRequestEditorDialog.kt
@@ -25,7 +25,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import me.him188.ani.app.ui.foundation.saveable.mutableStateSaver
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_continue_editing
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_discard
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_discard_confirmation
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_invalid_request
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_save_and_refresh
+import me.him188.ani.app.ui.lang.mediafetch_request_editor_title
+import me.him188.ani.app.ui.lang.settings_danmaku_cancel
import me.him188.ani.datasources.api.source.MediaFetchRequest
+import org.jetbrains.compose.resources.stringResource
/**
* @see MediaFetchRequestEditor
@@ -53,6 +62,13 @@ fun MediaFetchRequestEditorDialog(
}
val toaster = LocalToaster.current
+ val invalidRequestText = stringResource(Lang.mediafetch_request_editor_invalid_request)
+ val saveAndRefreshText = stringResource(Lang.mediafetch_request_editor_save_and_refresh)
+ val cancelText = stringResource(Lang.settings_danmaku_cancel)
+ val editRequestTitle = stringResource(Lang.mediafetch_request_editor_title)
+ val discardText = stringResource(Lang.mediafetch_request_editor_discard)
+ val continueEditingText = stringResource(Lang.mediafetch_request_editor_continue_editing)
+ val discardConfirmationText = stringResource(Lang.mediafetch_request_editor_discard_confirmation)
AlertDialog(
onDismissRequestWrapped,
@@ -62,20 +78,20 @@ fun MediaFetchRequestEditorDialog(
editingRequest.toMediaFetchRequestOrNull()?.let {
onDismissRequestWrapped()
onFetchRequestChange(it)
- } ?: toaster.toast("请求无效,请检查")
+ } ?: toaster.toast(invalidRequestText)
},
enabled = editingRequest.toMediaFetchRequestOrNull() != null,
) {
- Text("保存并刷新")
+ Text(saveAndRefreshText)
}
},
dismissButton = {
TextButton(onDismissRequestWrapped) {
- Text("取消")
+ Text(cancelText)
}
},
title = {
- Text("编辑查询请求")
+ Text(editRequestTitle)
},
text = {
MediaFetchRequestEditor(
@@ -98,7 +114,7 @@ fun MediaFetchRequestEditorDialog(
onDismissRequest()
},
) {
- Text("舍弃", color = MaterialTheme.colorScheme.error)
+ Text(discardText, color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
@@ -107,7 +123,7 @@ fun MediaFetchRequestEditorDialog(
showConfirmDiscard = false
},
) {
- Text("继续编辑")
+ Text(continueEditingText)
}
},
icon = {
@@ -117,7 +133,7 @@ fun MediaFetchRequestEditorDialog(
)
},
text = {
- Text("有未保存的编辑,要舍弃编辑吗?")
+ Text(discardConfirmationText)
},
)
}
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediaselect/selector/MediaSelectorWebColumn.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediaselect/selector/MediaSelectorWebColumn.kt
index 1c2ab23214..94da5f4392 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediaselect/selector/MediaSelectorWebColumn.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediaselect/selector/MediaSelectorWebColumn.kt
@@ -9,8 +9,8 @@
package me.him188.ani.app.ui.mediaselect.selector
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -50,9 +50,15 @@ import me.him188.ani.app.domain.mediasource.web.WebCaptchaRequest
import me.him188.ani.app.ui.foundation.IconButton
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.ifThen
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.media_selector_web_edit_query_action
+import me.him188.ani.app.ui.lang.media_selector_web_edit_query_prompt
+import me.him188.ani.app.ui.lang.media_selector_web_waiting_captcha
+import me.him188.ani.app.ui.lang.settings_mediasource_refresh
import me.him188.ani.app.ui.mediaselect.common.SourceIcon
import me.him188.ani.datasources.api.Media
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
data class WebSourceChannel(
@@ -95,6 +101,8 @@ fun MediaSelectorWebSourcesColumn(
modifier: Modifier = Modifier,
preferredSourceContainerColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.33f)
) {
+ val editQueryPromptText = stringResource(Lang.media_selector_web_edit_query_prompt)
+ val editQueryActionText = stringResource(Lang.media_selector_web_edit_query_action)
val card = @Composable { source: WebSource ->
WebSourceCard(
source,
@@ -133,13 +141,13 @@ fun MediaSelectorWebSourcesColumn(
) {
Text(
buildAnnotatedString {
- append("都不对?")
+ append(editQueryPromptText)
pushStyle(
SpanStyle(
textDecoration = TextDecoration.Underline,
),
)
- append("修改查询")
+ append(editQueryActionText)
},
color = MaterialTheme.colorScheme.outline,
textAlign = TextAlign.Center,
@@ -159,6 +167,8 @@ private fun WebSourceCard(
) {
val minHeight = 48.dp
+ val waitingCaptchaText = stringResource(Lang.media_selector_web_waiting_captcha)
+ val refreshText = stringResource(Lang.settings_mediasource_refresh)
Row(
modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -173,7 +183,7 @@ private fun WebSourceCard(
)
Box(Modifier.padding(start = 8.dp)) {
Text(
- "五个字占位",
+ source.name,
Modifier.alpha(0f).width(IntrinsicSize.Max),
softWrap = true,
maxLines = 2,
@@ -195,7 +205,7 @@ private fun WebSourceCard(
) {
if (source.isCaptchaRequired) {
Text(
- text = if (source.isResolvingCaptcha) "正在等待验证码处理" else source.captchaMessage.orEmpty(),
+ text = if (source.isResolvingCaptcha) waitingCaptchaText else source.captchaMessage.orEmpty(),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
@@ -242,7 +252,7 @@ private fun WebSourceCard(
if (source.isError) {
IconButton(onRefresh) {
- Icon(Icons.Rounded.Refresh, "刷新")
+ Icon(Icons.Rounded.Refresh, refreshText)
}
}
}
diff --git a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediaselect/summary/MediaSelectorSummary.kt b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediaselect/summary/MediaSelectorSummary.kt
index a35551a9d6..da48a6fc73 100644
--- a/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediaselect/summary/MediaSelectorSummary.kt
+++ b/app/shared/ui-mediaselect/src/commonMain/kotlin/ui/mediaselect/summary/MediaSelectorSummary.kt
@@ -66,7 +66,15 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.media_selector_summary_auto_selecting
+import me.him188.ani.app.ui.lang.media_selector_summary_change
+import me.him188.ani.app.ui.lang.media_selector_summary_manual_select
+import me.him188.ani.app.ui.lang.media_selector_summary_searched
+import me.him188.ani.app.ui.lang.media_selector_summary_select_source
+import me.him188.ani.app.ui.lang.media_selector_summary_source
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -153,11 +161,16 @@ fun MediaSelectorSummaryCard(
animationState.currentSummary,
transitionSpec = transitionSpec,
) { state ->
+ val manualSelectText = stringResource(Lang.media_selector_summary_manual_select)
+ val changeText = stringResource(Lang.media_selector_summary_change)
+ val autoSelectingText = stringResource(Lang.media_selector_summary_auto_selecting)
+ val selectSourceText = stringResource(Lang.media_selector_summary_select_source)
+ val sourceText = stringResource(Lang.media_selector_summary_source)
val button = @Composable {
val buttonLabel = when (state) {
- is MediaSelectorSummary.AutoSelecting -> "手动选择"
- is MediaSelectorSummary.RequiresManualSelection -> "手动选择"
- is MediaSelectorSummary.Selected -> "更换"
+ is MediaSelectorSummary.AutoSelecting -> manualSelectText
+ is MediaSelectorSummary.RequiresManualSelection -> manualSelectText
+ is MediaSelectorSummary.Selected -> changeText
}
OutlinedButton(
@@ -178,7 +191,7 @@ fun MediaSelectorSummaryCard(
when (state) {
is MediaSelectorSummary.AutoSelecting -> {
ListItem(
- headlineContent = { Text("正在自动选择数据源") },
+ headlineContent = { Text(autoSelectingText) },
commonModifiers,
leadingContent = {
LoadingIndicator(
@@ -191,7 +204,7 @@ fun MediaSelectorSummaryCard(
is MediaSelectorSummary.RequiresManualSelection -> {
ListItem(
- headlineContent = { Text("请选择数据源") },
+ headlineContent = { Text(selectSourceText) },
commonModifiers,
colors = listItemColors,
)
@@ -201,7 +214,7 @@ fun MediaSelectorSummaryCard(
ListItem(
headlineContent = { Text(state.source.sourceName, softWrap = true, maxLines = 2) },
commonModifiers,
- overlineContent = { Text("数据源") },
+ overlineContent = { Text(sourceText) },
leadingContent = {
SourceIcon(
state.source,
@@ -351,9 +364,10 @@ private fun SourceSummaryRow(
sources: List,
modifier: Modifier = Modifier,
) {
+ val searchedText = stringResource(Lang.media_selector_summary_searched)
Row(modifier, horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
- "已查找:",
+ searchedText,
softWrap = false,
style = MaterialTheme.typography.labelMedium,
color = contentColorFor(MaterialTheme.colorScheme.surfaceContainerHigh),
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginScreenLayout.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginScreenLayout.kt
index 3b9f926b28..59ee4761aa 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginScreenLayout.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginScreenLayout.kt
@@ -42,8 +42,10 @@ import me.him188.ani.app.ui.foundation.ifThen
import me.him188.ani.app.ui.foundation.layout.AniWindowInsets
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
+import org.jetbrains.compose.resources.*
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -53,7 +55,7 @@ internal fun EmailLoginScreenLayout(
onNavigateSettings: () -> Unit,
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier,
- title: @Composable () -> Unit = { Text("登录") },
+ title: @Composable () -> Unit = { Text(stringResource(Lang.login_sign_in)) },
showThirdPartyLogin: Boolean = true,
content: @Composable ColumnScope.(scrollState: ScrollState) -> Unit,
) {
@@ -65,7 +67,11 @@ internal fun EmailLoginScreenLayout(
title = title,
navigationIcon = { BackNavigationIconButton(onNavigateBack) },
scrollBehavior = scrollBehavior,
- actions = { IconButton(onNavigateSettings) { Icon(Icons.Outlined.Settings, "设置") } },
+ actions = {
+ IconButton(onNavigateSettings) {
+ Icon(Icons.Outlined.Settings, stringResource(Lang.settings))
+ }
+ },
windowInsets = AniWindowInsets.forTopAppBar(),
)
},
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginStartScreen.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginStartScreen.kt
index c2af2866d3..7b851082f3 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginStartScreen.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginStartScreen.kt
@@ -41,6 +41,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.rememberAsyncHandler
+import me.him188.ani.app.ui.lang.*
+import org.jetbrains.compose.resources.*
@Composable
fun EmailLoginStartScreen(
@@ -82,7 +84,7 @@ internal fun EmailLoginStartScreenImpl(
onNavigateSettings: () -> Unit,
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier,
- title: @Composable () -> Unit = { Text("登录") },
+ title: @Composable () -> Unit = { Text(stringResource(Lang.login_sign_in)) },
enabled: Boolean = true,
showThirdPartyLogin: Boolean = true,
) {
@@ -95,8 +97,8 @@ internal fun EmailLoginStartScreenImpl(
showThirdPartyLogin,
) { scrollState ->
CenteredSectionHeader(
- title = { Text("你的邮箱地址") },
- description = { Text("我们将发送一封验证码邮件") },
+ title = { Text(stringResource(Lang.login_email_address_title)) },
+ description = { Text(stringResource(Lang.login_email_address_description)) },
)
Spacer(Modifier.height(8.dp))
@@ -107,7 +109,7 @@ internal fun EmailLoginStartScreenImpl(
{ currentEmailContent = it.trim() },
Modifier.fillMaxWidth(),
label = {
- Text("邮箱")
+ Text(stringResource(Lang.login_email_label))
},
isError = currentEmailContent.isNotEmpty() &&
(!currentEmailContent.contains('@') || !currentEmailContent.contains('.')),
@@ -123,7 +125,7 @@ internal fun EmailLoginStartScreenImpl(
trailingIcon = if (currentEmailContent.isNotEmpty()) {
{
IconButton({ currentEmailContent = "" }) {
- Icon(Icons.Outlined.Close, "清空")
+ Icon(Icons.Outlined.Close, stringResource(Lang.login_clear))
}
}
} else null,
@@ -140,7 +142,7 @@ internal fun EmailLoginStartScreenImpl(
) {
Icon(Icons.AutoMirrored.Rounded.ArrowForward, null, Modifier.size(ButtonDefaults.IconSize))
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
- Text("继续")
+ Text(stringResource(Lang.login_continue))
}
}
}
@@ -149,13 +151,13 @@ internal fun EmailLoginStartScreenImpl(
internal fun EmailPageTitle(mode: EmailLoginUiState.Mode, isExistingAccount: Boolean?) {
when (mode) {
EmailLoginUiState.Mode.LOGIN -> when (isExistingAccount) {
- true -> Text("登录")
- false -> Text("注册")
- null -> Text("登录 / 注册")
+ true -> Text(stringResource(Lang.login_sign_in))
+ false -> Text(stringResource(Lang.login_sign_up))
+ null -> Text(stringResource(Lang.login_sign_in_or_sign_up))
}
- EmailLoginUiState.Mode.BIND -> Text("绑定邮箱")
- EmailLoginUiState.Mode.REBIND -> Text("更改邮箱")
+ EmailLoginUiState.Mode.BIND -> Text(stringResource(Lang.login_bind_email))
+ EmailLoginUiState.Mode.REBIND -> Text(stringResource(Lang.login_change_email))
}
}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginVerifyScreen.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginVerifyScreen.kt
index 54d3989412..07254a2f53 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginVerifyScreen.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/EmailLoginVerifyScreen.kt
@@ -43,7 +43,9 @@ import kotlinx.coroutines.delay
import me.him188.ani.app.data.repository.user.UserRepository.SendOtpResult
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.rememberAsyncHandler
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import org.jetbrains.compose.resources.*
import kotlin.time.Clock
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Instant
@@ -60,6 +62,8 @@ fun EmailLoginVerifyScreen(
val state by vm.state.collectAsStateWithLifecycle(EmailLoginUiState.Initial)
val asyncHandler = rememberAsyncHandler()
val toaster = LocalToaster.current
+ val emailAlreadyUsedText = stringResource(Lang.login_email_already_used)
+ val invalidOtpText = stringResource(Lang.login_invalid_otp)
EmailLoginVerifyScreenImpl(
@@ -75,8 +79,8 @@ fun EmailLoginVerifyScreen(
}
when (result) {
- SendOtpResult.EmailAlreadyExist -> toaster.show("该邮箱已被使用")
- SendOtpResult.InvalidOtp -> toaster.show("验证码无效或已过期,请重新发送")
+ SendOtpResult.EmailAlreadyExist -> toaster.show(emailAlreadyUsedText)
+ SendOtpResult.InvalidOtp -> toaster.show(invalidOtpText)
is SendOtpResult.Success -> onSuccess()
}
}
@@ -108,7 +112,7 @@ internal fun EmailLoginVerifyScreenImpl(
onNavigateSettings: () -> Unit,
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier,
- title: @Composable () -> Unit = { Text("登录") },
+ title: @Composable () -> Unit = { Text(stringResource(Lang.login_sign_in)) },
enabled: Boolean = true,
showThirdPartyLogin: Boolean = true,
) {
@@ -121,14 +125,18 @@ internal fun EmailLoginVerifyScreenImpl(
showThirdPartyLogin = showThirdPartyLogin,
) {
CenteredSectionHeader(
- title = { Text("输入验证码") },
+ title = { Text(stringResource(Lang.login_verify_title)) },
description = {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text("请检查邮箱 $email")
+ Text(stringResource(Lang.login_verify_check_email, email))
if (isExistingAccount != null) {
Spacer(Modifier.height(2.dp))
Text(
- if (isExistingAccount) "正在登录现有账号" else "正在注册新账号",
+ if (isExistingAccount) {
+ stringResource(Lang.login_verify_signing_in_existing)
+ } else {
+ stringResource(Lang.login_verify_signing_up_new)
+ },
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@@ -149,11 +157,11 @@ internal fun EmailLoginVerifyScreenImpl(
},
Modifier.fillMaxWidth(),
label = {
- Text("验证码")
+ Text(stringResource(Lang.login_verification_code))
},
isError = code.any { !it.isDigit() },
placeholder = {
- Text("6 位数字")
+ Text(stringResource(Lang.login_six_digit_number))
},
keyboardOptions = KeyboardOptions.Default.copy(
autoCorrectEnabled = false,
@@ -170,7 +178,7 @@ internal fun EmailLoginVerifyScreenImpl(
trailingIcon = if (code.isNotEmpty()) {
{
IconButton({ code = "" }) {
- Icon(Icons.Outlined.Close, "清空")
+ Icon(Icons.Outlined.Close, stringResource(Lang.login_clear))
}
}
} else null,
@@ -195,9 +203,9 @@ internal fun EmailLoginVerifyScreenImpl(
enabled = enabled && canResend,
) {
if (canResend) {
- Text("重新发送验证码")
+ Text(stringResource(Lang.login_resend_otp))
} else {
- Text("${timeLeft.inWholeSeconds} 秒后可重新发送")
+ Text(stringResource(Lang.login_resend_after_seconds, timeLeft.inWholeSeconds))
}
}
}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/ThirdPartyLoginMethods.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/ThirdPartyLoginMethods.kt
index d75ae85545..57af723440 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/ThirdPartyLoginMethods.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/login/ThirdPartyLoginMethods.kt
@@ -37,6 +37,8 @@ import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.icons.BangumiNext
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
+import me.him188.ani.app.ui.lang.*
+import org.jetbrains.compose.resources.*
@Composable
@@ -48,7 +50,7 @@ internal fun ThirdPartyLoginMethods(
TextDivider(
modifier = Modifier.heightIn(min = 56.dp),
) {
- Text("其他登录方式")
+ Text(stringResource(Lang.login_other_methods))
}
FilledTonalButton(
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/oauth/BangumiAuthorizeLayout.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/oauth/BangumiAuthorizeLayout.kt
index 6203554c73..bbf113dde3 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/oauth/BangumiAuthorizeLayout.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/oauth/BangumiAuthorizeLayout.kt
@@ -60,11 +60,13 @@ import me.him188.ani.app.ui.foundation.animation.AnimatedVisibilityMotionScheme
import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.icons.BangumiNext
import me.him188.ani.app.ui.foundation.icons.BangumiNextIconColor
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.widgets.HeroIcon
import me.him188.ani.app.ui.search.renderLoadErrorMessage
import me.him188.ani.app.ui.settings.SettingsTab
import me.him188.ani.app.ui.settings.framework.components.SettingsScope
import me.him188.ani.app.ui.settings.framework.components.TextItem
+import org.jetbrains.compose.resources.*
sealed interface AuthState {
data class LoggedInAni(val bound: Boolean) : Idle
@@ -108,7 +110,7 @@ fun BangumiAuthorizeLayout(
.fillMaxWidth(),
) {
Text(
- "授权 Bangumi 账号,可以同步你的观看记录到 Bangumi 或便捷登录 Ani",
+ stringResource(Lang.oauth_bangumi_description),
style = MaterialTheme.typography.titleMedium,
)
}
@@ -158,11 +160,11 @@ private fun AuthorizeButton(
) {
when (it) {
is AuthState.LoggedInAni -> {
- Text("绑定 Bangumi 账号")
+ Text(stringResource(Lang.oauth_bangumi_bind_account))
}
is AuthState.Idle, is AuthState.Failed -> {
- Text("登录 / 注册")
+ Text(stringResource(Lang.oauth_bangumi_sign_in_or_sign_up))
}
is AuthState.AwaitingResult -> {
@@ -174,12 +176,12 @@ private fun AuthorizeButton(
modifier = Modifier.size(16.dp),
strokeWidth = 3.dp,
)
- Text("正在等待结果")
+ Text(stringResource(Lang.oauth_bangumi_waiting_result))
}
}
is AuthState.Success -> {
- Text("已授权")
+ Text(stringResource(Lang.oauth_bangumi_authorized))
}
}
}
@@ -214,7 +216,7 @@ private fun AuthorizeButton(
) {
FilledTonalButton(
onClick = onClickCancel,
- content = { Text("取消") },
+ content = { Text(stringResource(Lang.oauth_bangumi_cancel)) },
shape = SplitButtonDefaults.trailingButtonShapesFor(48.dp).shape,
)
}
@@ -263,13 +265,13 @@ private enum class HelpOption {
@Composable
private fun renderHelpOptionTitle(option: HelpOption): String {
return when (option) {
- HelpOption.BANGUMI_DESC -> "Bangumi 是什么"
- HelpOption.WEBSITE_BLOCKED -> "浏览器提示网站被屏蔽或禁止访问"
- HelpOption.BANGUMI_REGISTER_CHOOSE -> "注册时应该选择哪一项"
- HelpOption.REGISTER_TYPE_WRONG_CAPTCHA -> "注册或登录时一直提示验证码错误"
- HelpOption.CANT_RECEIVE_REGISTER_EMAIL -> "无法收到邮箱验证码"
- HelpOption.REGISTER_ACTIVATION_FAILED -> "注册时一直激活失败"
- HelpOption.OTHERS -> "其他问题"
+ HelpOption.BANGUMI_DESC -> stringResource(Lang.oauth_bangumi_help_bangumi_desc)
+ HelpOption.WEBSITE_BLOCKED -> stringResource(Lang.oauth_bangumi_help_website_blocked)
+ HelpOption.BANGUMI_REGISTER_CHOOSE -> stringResource(Lang.oauth_bangumi_help_register_choose)
+ HelpOption.REGISTER_TYPE_WRONG_CAPTCHA -> stringResource(Lang.oauth_bangumi_help_wrong_captcha)
+ HelpOption.CANT_RECEIVE_REGISTER_EMAIL -> stringResource(Lang.oauth_bangumi_help_cant_receive_email)
+ HelpOption.REGISTER_ACTIVATION_FAILED -> stringResource(Lang.oauth_bangumi_help_activation_failed)
+ HelpOption.OTHERS -> stringResource(Lang.oauth_bangumi_help_others)
}
}
@@ -282,38 +284,34 @@ private fun RenderHelpOptionContent(
Box(modifier) {
when (option) {
HelpOption.BANGUMI_DESC -> {
-
- Text(
- "Bangumi 番组计划 是一个中文互联网的 ACGN 内容分享与交流网站,致力于提供一个轻松便捷独特的交流与沟通环境。\n" +
- "Bangumi 提供了番剧索引、番剧收藏、追番进等功能,Ani 可以将你的观看记录同步至 Bangumi。",
- )
+ Text(stringResource(Lang.oauth_bangumi_help_bangumi_desc_content))
}
HelpOption.WEBSITE_BLOCKED -> {
- Text("请在系统设置中更换默认浏览器,推荐按使用 Google Chrome,Microsoft Edge 或 Mozilla Firefox 浏览器")
+ Text(stringResource(Lang.oauth_bangumi_help_website_blocked_content))
}
HelpOption.BANGUMI_REGISTER_CHOOSE -> {
- Text("管理 ACG 收藏与收视进度,分享交流")
+ Text(stringResource(Lang.oauth_bangumi_help_register_choose_content))
}
HelpOption.REGISTER_TYPE_WRONG_CAPTCHA -> {
- Text("如果没有验证码的输入框,可以尝试多点几次密码输入框,如果输错了验证码,需要刷新页面再登录")
+ Text(stringResource(Lang.oauth_bangumi_help_wrong_captcha_content))
}
HelpOption.CANT_RECEIVE_REGISTER_EMAIL -> {
- Text("请检查垃圾箱,并且尽可能使用常见邮箱注册,例如 QQ, 网易, Outlook")
+ Text(stringResource(Lang.oauth_bangumi_help_cant_receive_email_content))
}
HelpOption.REGISTER_ACTIVATION_FAILED -> {
- Text("删除激活码的最后一个字,然后手动输入删除的字,或更换其他浏览器")
+ Text(stringResource(Lang.oauth_bangumi_help_activation_failed_content))
}
HelpOption.OTHERS -> {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text("无法解决你的问题?还可以通过以下渠道获取帮助")
+ Text(stringResource(Lang.oauth_bangumi_help_others_content))
contactActions()
}
}
@@ -336,7 +334,7 @@ private fun SettingsScope.AuthorizeHelpQA(
horizontalAlignment = Alignment.Start,
) {
Text(
- "帮助",
+ stringResource(Lang.oauth_bangumi_help_title),
style = MaterialTheme.typography.headlineSmall,
)
}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/oauth/BangumiAuthorizeScreen.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/oauth/BangumiAuthorizeScreen.kt
index 60c9b58011..5db9e9756d 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/oauth/BangumiAuthorizeScreen.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/oauth/BangumiAuthorizeScreen.kt
@@ -19,6 +19,8 @@ import kotlinx.coroutines.launch
import me.him188.ani.app.platform.LocalContext
import me.him188.ani.app.platform.navigation.rememberAsyncBrowserNavigator
import me.him188.ani.app.ui.login.EmailLoginScreenLayout
+import me.him188.ani.app.ui.lang.*
+import org.jetbrains.compose.resources.*
@Composable
fun BangumiAuthorizeScreen(
@@ -73,7 +75,7 @@ internal fun BangumiAuthorizeScreen(
onBangumiLoginClick = {},
onNavigateSettings = onNavigateSettings,
onNavigateBack = onNavigateBack,
- title = { Text("授权 Bangumi 登录") },
+ title = { Text(stringResource(Lang.oauth_bangumi_authorize_title)) },
showThirdPartyLogin = false,
) { scrollState ->
BangumiAuthorizeLayout(
@@ -84,4 +86,4 @@ internal fun BangumiAuthorizeScreen(
scrollState = scrollState,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/OnboardingCompleteScreen.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/OnboardingCompleteScreen.kt
index 01b9808d66..501e1a3745 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/OnboardingCompleteScreen.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/OnboardingCompleteScreen.kt
@@ -49,9 +49,11 @@ import me.him188.ani.app.ui.foundation.avatar.AvatarImage
import me.him188.ani.app.ui.foundation.layout.AniWindowInsets
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.user.SelfInfoUiState
import me.him188.ani.app.ui.user.TestSelfInfoUiState
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.*
@Composable
fun OnboardingCompleteScreen(
@@ -111,8 +113,11 @@ internal fun OnboardingCompleteScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
+ val welcomeText = state.selfInfo?.nickname?.let {
+ stringResource(Lang.onboarding_complete_welcome_with_nickname, it)
+ } ?: stringResource(Lang.onboarding_complete_welcome)
Text(
- text = if (state.selfInfo?.nickname != null) "欢迎,${state.selfInfo?.nickname}" else "欢迎",
+ text = welcomeText,
modifier = Modifier
.widthIn(max = 240.dp)
.animateContentSize(),
@@ -139,7 +144,7 @@ internal fun OnboardingCompleteScreen(
Modifier.size(ButtonDefaults.IconSize),
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text("完成", softWrap = false)
+ Text(stringResource(Lang.onboarding_complete_finish), softWrap = false)
}
}
}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/OnboardingScreen.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/OnboardingScreen.kt
index 3a02a71383..b60ab328a9 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/OnboardingScreen.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/OnboardingScreen.kt
@@ -34,6 +34,7 @@ import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.layout.AniWindowInsets
import me.him188.ani.app.ui.foundation.stateOf
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.onboarding.navigation.WizardController
import me.him188.ani.app.ui.onboarding.navigation.WizardDefaults
import me.him188.ani.app.ui.onboarding.navigation.WizardNavHost
@@ -56,6 +57,7 @@ import me.him188.ani.utils.analytics.Analytics
import me.him188.ani.utils.analytics.AnalyticsEvent
import me.him188.ani.utils.analytics.AnalyticsEvent.Companion.OnboardingStart
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.*
@Composable
fun OnboardingScreen(
@@ -127,7 +129,7 @@ fun OnboardingScreen(
) {
step(
"theme",
- { Text("主题设置") },
+ { Text(stringResource(Lang.onboarding_theme_settings)) },
navigationIcon = navigationIcon,
) {
val themeSelectUiState by state.themeSelectState.state
@@ -142,7 +144,7 @@ fun OnboardingScreen(
}
step(
"proxy",
- title = { Text("网络设置") },
+ title = { Text(stringResource(Lang.onboarding_network_settings)) },
autoSkip = { proxyState.overallState == ProxyOverallTestState.SUCCESS },
forwardButton = {
WizardDefaults.GoForwardButton(
@@ -186,7 +188,7 @@ fun OnboardingScreen(
}
/*step(
"bittorrent",
- { Text("BT 播放和缓存") },
+ { Text("BT Playback and Cache") },
forwardButton = {
WizardDefaults.GoForwardButton(
{
@@ -303,4 +305,4 @@ fun PreviewOnboardingScene() {
onFinishOnboarding = { },
)
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/WelcomeScreen.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/WelcomeScreen.kt
index 3a7256bf52..6fd9634b14 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/WelcomeScreen.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/WelcomeScreen.kt
@@ -41,7 +41,9 @@ import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.animation.WithContentEnterAnimation
import me.him188.ani.app.ui.foundation.layout.AniWindowInsets
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
+import org.jetbrains.compose.resources.*
@Composable
fun WelcomeScreen(
@@ -93,12 +95,12 @@ internal fun WelcomeScene(
Column(
verticalArrangement = Arrangement.Center,
) {
- Text("欢迎使用 Animeko", style = MaterialTheme.typography.headlineMedium)
+ Text(stringResource(Lang.onboarding_welcome_title), style = MaterialTheme.typography.headlineMedium)
ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
Row(Modifier.padding(top = 8.dp).align(Alignment.Start)) {
Text(
- """一站式在线弹幕追番平台 (简称 Ani)""",
+ stringResource(Lang.onboarding_welcome_subtitle),
color = MaterialTheme.colorScheme.primary,
)
}
@@ -109,9 +111,9 @@ internal fun WelcomeScene(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
- Text("""Ani 目前由爱好者组成的组织 OpenAni 和社区贡献者维护,完全免费,在 GitHub 上开源。""")
+ Text(stringResource(Lang.onboarding_welcome_maintained))
- Text("""Ani 的目标是提供尽可能简单且舒适的追番体验。""")
+ Text(stringResource(Lang.onboarding_welcome_goal))
}
}
@@ -127,7 +129,7 @@ internal fun WelcomeScene(
onClick = onClickContinue,
modifier = Modifier.widthIn(300.dp),
) {
- Text("继续")
+ Text(stringResource(Lang.onboarding_welcome_continue))
}
}
}
@@ -170,4 +172,4 @@ private fun TestContactActions(
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/navigation/WizardNavHost.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/navigation/WizardNavHost.kt
index 719d727212..a0fac95a80 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/navigation/WizardNavHost.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/navigation/WizardNavHost.kt
@@ -65,6 +65,8 @@ import me.him188.ani.app.ui.foundation.animation.NavigationMotionScheme
import me.him188.ani.app.ui.foundation.layout.AniWindowInsets
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
+import me.him188.ani.app.ui.lang.*
+import org.jetbrains.compose.resources.*
/**
* A wrapper around [NavHost] that provides a wizard-like experience.
@@ -201,8 +203,9 @@ private suspend fun animateScrollTopAppBar(topAppBarState: TopAppBarState, targe
}
object WizardDefaults {
+ @Composable
fun renderStepIndicatorText(currentStep: Int, totalStep: Int): String {
- return "步骤 $currentStep / $totalStep"
+ return stringResource(Lang.onboarding_navigation_step_indicator, currentStep, totalStep)
}
@Composable
@@ -231,9 +234,7 @@ object WizardDefaults {
MaterialTheme.colorScheme.primary,
) {
Text(
- text = remember(currentStep, totalStep) {
- renderStepIndicatorText(currentStep, totalStep)
- },
+ text = renderStepIndicatorText(currentStep, totalStep),
modifier = Modifier.testTag(indicatorStepTextTestTag),
)
}
@@ -281,15 +282,16 @@ object WizardDefaults {
enabled: Boolean,
modifier: Modifier = Modifier,
colors: ButtonColors = ButtonDefaults.buttonColors(),
- text: String = "下一步"
+ text: String? = null
) {
+ val buttonText = text ?: stringResource(Lang.onboarding_navigation_next)
Button(
onClick = onClick,
enabled = enabled,
modifier = modifier,
colors = colors,
) {
- Text(text)
+ Text(buttonText)
}
}
@@ -297,27 +299,29 @@ object WizardDefaults {
fun GoBackwardButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
- text: String = "上一步"
+ text: String? = null
) {
+ val buttonText = text ?: stringResource(Lang.onboarding_navigation_previous)
OutlinedButton(
onClick = onClick,
modifier = modifier,
) {
- Text(text)
+ Text(buttonText)
}
}
@Composable
fun SkipButton(
onClick: () -> Unit,
- text: String = "跳过",
+ text: String? = null,
modifier: Modifier = Modifier
) {
+ val buttonText = text ?: stringResource(Lang.onboarding_navigation_skip)
TextButton(
onClick = onClick,
modifier = modifier,
) {
- Text(text)
+ Text(buttonText)
}
}
}
@@ -332,7 +336,7 @@ fun PreviewWizardNavHost() {
) {
step(
key = "theme",
- title = { Text("选择主题") },
+ title = { Text(stringResource(Lang.onboarding_navigation_choose_theme)) },
) {
val data = remember {
TestWizardData.MyTheme("theme default", 0)
@@ -347,7 +351,7 @@ fun PreviewWizardNavHost() {
step(
key = "proxy",
- title = { Text("设置代理") },
+ title = { Text(stringResource(Lang.onboarding_navigation_configure_proxy)) },
) {
val data = remember {
TestWizardData.MyProxy("proxy default", 0)
@@ -363,7 +367,7 @@ fun PreviewWizardNavHost() {
step(
key = "bit_torrent",
- title = { Text("BitTorrent 功能") },
+ title = { Text(stringResource(Lang.onboarding_navigation_bittorrent)) },
) {
val data = remember {
TestWizardData.MyBitTorrent("bittorrent default", 0)
@@ -378,7 +382,7 @@ fun PreviewWizardNavHost() {
step(
key = "finish",
- title = { Text("完成") },
+ title = { Text(stringResource(Lang.onboarding_complete_finish)) },
) {
Text("Finish")
}
@@ -391,4 +395,4 @@ private sealed class TestWizardData {
data class MyProxy(val proxy: String, val counter: Int) : TestWizardData()
data class MyBitTorrent(val bittorrent: String, val counter: Int) : TestWizardData()
data object MyFinish : TestWizardData()
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/step/BitTorrentFeatureStep.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/step/BitTorrentFeatureStep.kt
index 5a225d1f72..5ff52fb48f 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/step/BitTorrentFeatureStep.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/step/BitTorrentFeatureStep.kt
@@ -41,10 +41,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.widgets.HeroIcon
import me.him188.ani.app.ui.onboarding.WizardLayoutParams
import me.him188.ani.app.ui.settings.SettingsTab
import me.him188.ani.app.ui.settings.rendering.P2p
+import org.jetbrains.compose.resources.*
@Composable
internal fun BitTorrentFeatureStep(
@@ -77,8 +79,8 @@ internal fun BitTorrentFeatureStep(
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
- Text("Ani 支持边下边播 BT 资源,BT 下载速度取决于网络质量")
- Text("允许通知权限,在缓存时查看下载进度")
+ Text(stringResource(Lang.onboarding_bittorrent_play_while_downloading))
+ Text(stringResource(Lang.onboarding_bittorrent_notification_permission_hint))
}
Column(
modifier = Modifier.padding(vertical = 16.dp),
@@ -122,7 +124,7 @@ internal fun BitTorrentFeatureSwitchItem(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- text = "启用 BitTorrent 功能",
+ text = stringResource(Lang.onboarding_bittorrent_enable),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
modifier = Modifier.basicMarquee(),
@@ -167,7 +169,7 @@ internal fun RequestNotificationPermission(
modifier = Modifier.size(32.dp),
)
Text(
- text = "允许通知",
+ text = stringResource(Lang.onboarding_bittorrent_allow_notification),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
modifier = Modifier.basicMarquee(),
@@ -175,7 +177,7 @@ internal fun RequestNotificationPermission(
}
Box(modifier = Modifier.padding(start = 48.dp)) {
Text(
- text = "显示 BT 下载进度和速度等信息",
+ text = stringResource(Lang.onboarding_bittorrent_notification_description),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -186,13 +188,13 @@ internal fun RequestNotificationPermission(
onClick = { },
enabled = false,
modifier = Modifier.fillMaxWidth(),
- content = { Text("已授权") },
+ content = { Text(stringResource(Lang.onboarding_bittorrent_authorized)) },
)
} else {
Button(
onClick = onRequestNotificationPermission,
modifier = Modifier.fillMaxWidth(),
- content = { Text("授予权限") },
+ content = { Text(stringResource(Lang.onboarding_bittorrent_grant_permission)) },
)
}
}
@@ -238,4 +240,4 @@ fun PreviewBitTorrentFeatureStep() {
},
)
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/step/ThemeSelectStep.kt b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/step/ThemeSelectStep.kt
index 98fce90b80..c5d537e53a 100644
--- a/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/step/ThemeSelectStep.kt
+++ b/app/shared/ui-onboarding/src/commonMain/kotlin/ui/onboarding/step/ThemeSelectStep.kt
@@ -30,6 +30,7 @@ import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
import me.him188.ani.app.ui.foundation.theme.isPlatformSupportDynamicTheme
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.onboarding.WizardLayoutParams
import me.him188.ani.app.ui.settings.SettingsTab
import me.him188.ani.app.ui.settings.framework.components.TextItem
@@ -37,6 +38,7 @@ import me.him188.ani.app.ui.settings.tabs.theme.ColorButton
import me.him188.ani.app.ui.settings.tabs.theme.DarkModeSelectPanel
import me.him188.ani.app.ui.theme.DefaultSeedColor
import me.him188.ani.app.ui.theme.themeColorOptions
+import org.jetbrains.compose.resources.*
@Composable
internal fun ThemeSelectStep(
@@ -56,7 +58,7 @@ internal fun ThemeSelectStep(
.padding(top = layoutParams.verticalPadding),
)
Group(
- title = { Text("色彩") },
+ title = { Text(stringResource(Lang.onboarding_theme_color)) },
useThinHeader = true,
) {
if (isPlatformSupportDynamicTheme()) {
@@ -64,8 +66,8 @@ internal fun ThemeSelectStep(
modifier = Modifier
.fillMaxWidth()
.clickable { onUpdateUseDynamicTheme(!config.useDynamicTheme) },
- title = { Text("动态色彩") },
- description = { Text("使用系统强调色") },
+ title = { Text(stringResource(Lang.onboarding_theme_dynamic_color)) },
+ description = { Text(stringResource(Lang.onboarding_theme_dynamic_color_desc)) },
action = {
Switch(
checked = config.useDynamicTheme,
@@ -121,4 +123,4 @@ fun PreviewSelectThemeStep() {
onUpdateSeedColor = { },
)
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/app/AppSettingsTab.android.kt b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/app/AppSettingsTab.android.kt
index 281517cbbb..49860dd020 100644
--- a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/app/AppSettingsTab.android.kt
+++ b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/app/AppSettingsTab.android.kt
@@ -22,6 +22,9 @@ import me.him188.ani.app.platform.LocalContext
import me.him188.ani.app.ui.lang.Lang
import me.him188.ani.app.ui.lang.SupportedLocales
import me.him188.ani.app.ui.lang.settings_app_language
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_app_danmaku_refresh_rate
+import me.him188.ani.app.ui.lang.settings_theme_mode_auto
import me.him188.ani.app.ui.settings.framework.SettingsState
import me.him188.ani.app.ui.settings.framework.components.DropdownItem
import me.him188.ani.app.ui.settings.framework.components.SettingsScope
@@ -74,7 +77,7 @@ actual fun SettingsScope.PlayerGroupPlatform(videoScaffoldConfig: SettingsState<
values = { supportedModes },
itemText = {
if (it == null) {
- Text("自动")
+ Text(stringResource(Lang.settings_theme_mode_auto))
} else {
Text(it.refreshRate.roundToInt().toString())
}
@@ -87,7 +90,7 @@ actual fun SettingsScope.PlayerGroupPlatform(videoScaffoldConfig: SettingsState<
)
},
title = {
- Text("弹幕刷新率")
+ Text(stringResource(Lang.settings_app_danmaku_refresh_rate))
},
)
}
diff --git a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/app/UISettingsTab.android.kt b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/app/UISettingsTab.android.kt
index 6431bb11a4..28f3dd8c72 100644
--- a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/app/UISettingsTab.android.kt
+++ b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/app/UISettingsTab.android.kt
@@ -30,9 +30,17 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import me.him188.ani.app.ui.foundation.LocalIsPreviewing
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_app_background_running
+import me.him188.ani.app.ui.lang.settings_app_background_running_description
+import me.him188.ani.app.ui.lang.settings_app_ignore_battery_optimizations
+import me.him188.ani.app.ui.lang.settings_app_ignore_battery_optimizations_description
+import me.him188.ani.app.ui.lang.settings_app_notification_settings
+import me.him188.ani.app.ui.lang.settings_app_open_settings
import me.him188.ani.app.ui.settings.framework.components.RowButtonItem
import me.him188.ani.app.ui.settings.framework.components.SettingsScope
import me.him188.ani.app.ui.settings.framework.components.SwitchItem
+import org.jetbrains.compose.resources.stringResource
@SuppressLint("BatteryLife")
@@ -51,8 +59,8 @@ internal actual fun SettingsScope.AppSettingsTabPlatform() {
// 禁用电池优化
if (powerManager != null) {
Group(
- title = { Text("后台运行") },
- description = { Text(text = "缓存功能需要应用保持在后台运行才能下载视频") },
+ title = { Text(stringResource(Lang.settings_app_background_running)) },
+ description = { Text(text = stringResource(Lang.settings_app_background_running_description)) },
) {
val isPreviewing = LocalIsPreviewing.current
var isIgnoring by remember {
@@ -89,14 +97,14 @@ internal actual fun SettingsScope.AppSettingsTabPlatform() {
}
}
},
- title = { Text("禁用电池优化") },
- description = { Text("可以帮助保持在后台运行。可能增加耗电") },
+ title = { Text(stringResource(Lang.settings_app_ignore_battery_optimizations)) },
+ description = { Text(stringResource(Lang.settings_app_ignore_battery_optimizations_description)) },
)
}
}
Group(
- title = { Text("通知设置") },
+ title = { Text(stringResource(Lang.settings_app_notification_settings)) },
) {
RowButtonItem(
icon = { Icon(Icons.Rounded.ArrowOutward, contentDescription = null) },
@@ -114,6 +122,6 @@ internal actual fun SettingsScope.AppSettingsTabPlatform() {
it.printStackTrace()
}
},
- ) { Text(text = "打开设置") }
+ ) { Text(text = stringResource(Lang.settings_app_open_settings)) }
}
}
diff --git a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/log/LogTab.android.kt b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/log/LogTab.android.kt
index 72d745d726..5bf6969437 100644
--- a/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/log/LogTab.android.kt
+++ b/app/shared/ui-settings/src/androidMain/kotlin/ui/settings/tabs/log/LogTab.android.kt
@@ -24,7 +24,12 @@ import androidx.core.content.FileProvider
import kotlinx.coroutines.launch
import me.him188.ani.app.platform.LocalContext
import me.him188.ani.app.ui.foundation.setClipEntryText
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_log_copy_today_log_content
+import me.him188.ani.app.ui.lang.settings_log_share_file
+import me.him188.ani.app.ui.lang.settings_log_share_today_log_file
import me.him188.ani.buildconfig.AndroidBuildConfig
+import org.jetbrains.compose.resources.stringResource
import java.io.File
@@ -33,9 +38,12 @@ internal actual fun ColumnScope.PlatformLoggingItems(listItemColors: ListItemCol
val context = LocalContext.current
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
+ val shareTodayLogFileText = stringResource(Lang.settings_log_share_today_log_file)
+ val shareLogFileText = stringResource(Lang.settings_log_share_file)
+ val copyTodayLogContentText = stringResource(Lang.settings_log_copy_today_log_content)
ListItem(
- headlineContent = { Text("分享当日日志文件") },
+ headlineContent = { Text(shareTodayLogFileText) },
Modifier.clickable {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.setType("text/plain") // Set appropriate MIME type
@@ -48,13 +56,13 @@ internal actual fun ColumnScope.PlatformLoggingItems(listItemColors: ListItemCol
),
)
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- context.startActivity(Intent.createChooser(shareIntent, "分享日志文件"))
+ context.startActivity(Intent.createChooser(shareIntent, shareLogFileText))
},
colors = listItemColors,
)
ListItem(
- headlineContent = { Text("复制当日日志内容 (很大)") },
+ headlineContent = { Text(copyTodayLogContentText) },
Modifier.clickable {
scope.launch {
clipboard.setClipEntryText(context.getCurrentLogFile().readText())
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/BangumiSyncTab.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/BangumiSyncTab.kt
index f8c3bfdfa2..917eefbef2 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/BangumiSyncTab.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/BangumiSyncTab.kt
@@ -43,6 +43,20 @@ import me.him188.ani.app.ui.foundation.AbstractViewModel
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.rememberAsyncHandler
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_account_bangumi_execute_all
+import me.him188.ani.app.ui.lang.settings_account_bangumi_manual_full_sync
+import me.him188.ani.app.ui.lang.settings_account_bangumi_pending_sync_ops
+import me.him188.ani.app.ui.lang.settings_account_bangumi_redownload_all_data
+import me.him188.ani.app.ui.lang.settings_account_bangumi_redownload_all_data_description
+import me.him188.ani.app.ui.lang.settings_account_bangumi_sync_delete_collection
+import me.him188.ani.app.ui.lang.settings_account_bangumi_sync_mark_episode_unwatched
+import me.him188.ani.app.ui.lang.settings_account_bangumi_sync_mark_episode_watched
+import me.him188.ani.app.ui.lang.settings_account_bangumi_sync_queue
+import me.him188.ani.app.ui.lang.settings_account_bangumi_sync_unknown_op
+import me.him188.ani.app.ui.lang.settings_account_bangumi_sync_update_collection
+import me.him188.ani.app.ui.lang.settings_account_loading
+import me.him188.ani.app.ui.lang.settings_account_loading_placeholder
import me.him188.ani.app.ui.search.createTestPager
import me.him188.ani.app.ui.search.loadErrorItem
import me.him188.ani.app.ui.search.pagingFooterStateItem
@@ -56,6 +70,7 @@ import me.him188.ani.utils.coroutines.flows.restartable
import me.him188.ani.utils.logging.trace
import me.him188.ani.utils.platform.Uuid
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.time.Clock
@@ -141,27 +156,36 @@ fun BangumiSyncTabImpl(
isBangumiSyncing: Boolean,
modifier: Modifier = Modifier,
) = SettingsTab(modifier) {
- Group({ Text("手动全量同步") }) {
+ val fullSyncText = stringResource(Lang.settings_account_bangumi_manual_full_sync)
+ val redownloadText = stringResource(Lang.settings_account_bangumi_redownload_all_data)
+ val redownloadDescriptionText = stringResource(Lang.settings_account_bangumi_redownload_all_data_description)
+ val syncQueueText = stringResource(Lang.settings_account_bangumi_sync_queue)
+ val pendingSyncOpsText = stringResource(Lang.settings_account_bangumi_pending_sync_ops)
+ val executeAllText = stringResource(Lang.settings_account_bangumi_execute_all)
+ val loadingText = stringResource(Lang.settings_account_loading)
+ val loadingPlaceholderText = stringResource(Lang.settings_account_loading_placeholder)
+
+ Group({ Text(fullSyncText) }) {
TextItem(
title = {
- Text("重新下载全部 Bangumi 数据")
+ Text(redownloadText)
},
onClick = onFullSyncClick,
onClickEnabled = !isBangumiSyncing,
description = {
- Text("将 Bangumi 的收藏数据下载到 Animeko 收藏服务。通常来说不需要进行这个操作,Animeko 能自动完成同步。仅在你有发现数据不一致的情况时才需要手动下载。此操作可能需要数分钟才能完成,在同步过程中其他功能不可用。请注意,每十分钟只能执行一次全量同步")
+ Text(redownloadDescriptionText)
},
)
}
val items = syncCommandsFlow.collectAsLazyPagingItems()
Group(
- { Text("同步队列") },
- description = { Text("待执行的同步操作") },
+ { Text(syncQueueText) },
+ description = { Text(pendingSyncOpsText) },
actions = {
TextButton(onPushClick, enabled = !isBangumiSyncing) {
Icon(Icons.Default.Publish, null, Modifier.size(ButtonDefaults.IconSize))
- Text("执行全部")
+ Text(executeAllText)
}
},
) {
@@ -178,12 +202,12 @@ fun BangumiSyncTabImpl(
TextItem(
title = {
Text(
- item?.let { describe(it.op) }
- ?: "加载中加载中加载中加载中...", // placeholder, no localization
+ item?.let { describeBangumiSyncOp(it.op) }
+ ?: loadingPlaceholderText,
)
},
description = {
- Text(item?.id ?: "加载中...")
+ Text(item?.id ?: loadingText)
},
modifier = Modifier.placeholder(item == null)
.animateItem(
@@ -207,22 +231,50 @@ fun BangumiSyncTabImpl(
}
}
-private fun describe(op: BangumiSyncOp?): String {
+@Composable
+private fun describeBangumiSyncOp(op: BangumiSyncOp?): String {
return when (op) {
- is BangumiSyncOp.AddCollection -> "更新收藏:${op.subjectId} (${op.type.name})"
- is BangumiSyncOp.DeleteCollection -> "删除收藏:${op.subjectId}"
+ is BangumiSyncOp.AddCollection -> stringResource(
+ Lang.settings_account_bangumi_sync_update_collection,
+ op.subjectId,
+ op.type.name,
+ )
+
+ is BangumiSyncOp.DeleteCollection -> stringResource(
+ Lang.settings_account_bangumi_sync_delete_collection,
+ op.subjectId,
+ )
is BangumiSyncOp.UpdateCollection -> {
- if (op.type == null) return "删除收藏:${op.subjectId}"
- "更新收藏:${op.subjectId} (${op.type?.name})"
+ val type = op.type
+ if (type == null) {
+ return stringResource(
+ Lang.settings_account_bangumi_sync_delete_collection,
+ op.subjectId,
+ )
+ }
+ stringResource(
+ Lang.settings_account_bangumi_sync_update_collection,
+ op.subjectId,
+ type.name,
+ )
}
is BangumiSyncOp.UpdateEpisodeCollection -> {
val type = op.type
- if (type == null) return "标记剧集为未看过:${op.episodeId}"
- "标记剧集为看过:${op.episodeId} (${type.name})"
+ if (type == null) {
+ return stringResource(
+ Lang.settings_account_bangumi_sync_mark_episode_unwatched,
+ op.episodeId,
+ )
+ }
+ stringResource(
+ Lang.settings_account_bangumi_sync_mark_episode_watched,
+ op.episodeId,
+ type.name,
+ )
}
- null -> "未知操作(请更新版本)"
+ null -> stringResource(Lang.settings_account_bangumi_sync_unknown_op)
}
}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfileGroup.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfileGroup.kt
index 5076942172..9dcc1aa33e 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfileGroup.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfileGroup.kt
@@ -79,6 +79,30 @@ import me.him188.ani.app.ui.foundation.layout.isWidthCompact
import me.him188.ani.app.ui.foundation.rememberAsyncHandler
import me.him188.ani.app.ui.foundation.rememberDragAndDropState
import me.him188.ani.app.ui.foundation.widgets.HeroIcon
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_account_profile_avatar_invalid_format
+import me.him188.ani.app.ui.lang.settings_account_profile_avatar_size_exceeded
+import me.him188.ani.app.ui.lang.settings_account_profile_bind
+import me.him188.ani.app.ui.lang.settings_account_profile_crop_and_upload
+import me.him188.ani.app.ui.lang.settings_account_profile_crop_avatar
+import me.him188.ani.app.ui.lang.settings_account_profile_crop_hint
+import me.him188.ani.app.ui.lang.settings_account_profile_done
+import me.him188.ani.app.ui.lang.settings_account_profile_email
+import me.him188.ani.app.ui.lang.settings_account_profile_nickname
+import me.him188.ani.app.ui.lang.settings_account_profile_nickname_hint
+import me.him188.ani.app.ui.lang.settings_account_profile_not_bound
+import me.him188.ani.app.ui.lang.settings_account_profile_not_set
+import me.him188.ani.app.ui.lang.settings_account_profile_select_avatar
+import me.him188.ani.app.ui.lang.settings_account_profile_select_file
+import me.him188.ani.app.ui.lang.settings_account_profile_select_file_description
+import me.him188.ani.app.ui.lang.settings_account_profile_select_file_description_desktop
+import me.him188.ani.app.ui.lang.settings_account_profile_third_party_accounts
+import me.him188.ani.app.ui.lang.settings_account_profile_unbind
+import me.him188.ani.app.ui.lang.settings_account_profile_unbind_bangumi_confirmation
+import me.him188.ani.app.ui.lang.settings_account_profile_upload_avatar
+import me.him188.ani.app.ui.lang.settings_account_profile_uploading_avatar
+import me.him188.ani.app.ui.lang.settings_account_profile_user_id
+import me.him188.ani.app.ui.lang.subject_collection_cancel
import me.him188.ani.app.ui.search.LoadErrorCard
import me.him188.ani.app.ui.search.LoadErrorCardLayout
import me.him188.ani.app.ui.search.LoadErrorCardRole
@@ -88,6 +112,7 @@ import me.him188.ani.app.ui.settings.framework.components.TextFieldItem
import me.him188.ani.app.ui.settings.framework.components.TextItem
import me.him188.ani.utils.platform.Platform
import me.him188.ani.utils.platform.currentPlatform
+import org.jetbrains.compose.resources.stringResource
// Crop helpers for free-drag 1:1 selector
private enum class CropCorner { TL, TR, BL, BR }
@@ -170,6 +195,15 @@ internal fun SettingsScope.ProfileGroupImpl(
val currentInfo = state.selfInfo.selfInfo
val currentState by rememberUpdatedState(state.selfInfo)
var showUploadAvatarDialog by rememberSaveable { mutableStateOf(false) }
+ val notSetText = stringResource(Lang.settings_account_profile_not_set)
+ val nicknameText = stringResource(Lang.settings_account_profile_nickname)
+ val nicknameHintText = stringResource(Lang.settings_account_profile_nickname_hint)
+ val emailText = stringResource(Lang.settings_account_profile_email)
+ val bindText = stringResource(Lang.settings_account_profile_bind)
+ val userIdText = stringResource(Lang.settings_account_profile_user_id)
+ val thirdPartyAccountsText = stringResource(Lang.settings_account_profile_third_party_accounts)
+ val notBoundText = stringResource(Lang.settings_account_profile_not_bound)
+ val unbindText = stringResource(Lang.settings_account_profile_unbind)
Column(modifier) {
Column(
@@ -201,9 +235,9 @@ internal fun SettingsScope.ProfileGroupImpl(
TextFieldItem(
value = currentInfo?.nickname.orEmpty(),
- title = { Text("昵称") },
- description = { Text(currentInfo?.nickname?.let { "@$it" } ?: "未设置") },
- textFieldDescription = { Text("最多 20 字,只能包含中文、日文、英文、数字和下划线") },
+ title = { Text(nicknameText) },
+ description = { Text(currentInfo?.nickname?.let { "@$it" } ?: notSetText) },
+ textFieldDescription = { Text(nicknameHintText) },
onValueChangeCompleted = { onSaveNickname(it) },
inverseTitleDescription = true,
isErrorProvider = { isNicknameErrorProvider(it) },
@@ -218,19 +252,19 @@ internal fun SettingsScope.ProfileGroupImpl(
title = {
SelectionContainer {
Text(
- currentInfo?.email ?: "未设置",
+ currentInfo?.email ?: notSetText,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
}
},
- description = { Text("邮箱") },
+ description = { Text(emailText) },
modifier = Modifier.placeholder(isPlaceholder),
onClick = if (canBindEmail) onNavigateToEmail else null,
action = if (canBindEmail) {
{
IconButton(onNavigateToEmail) {
- Icon(Icons.Rounded.Edit, "绑定", tint = MaterialTheme.colorScheme.primary)
+ Icon(Icons.Rounded.Edit, bindText, tint = MaterialTheme.colorScheme.primary)
}
}
} else null,
@@ -241,21 +275,21 @@ internal fun SettingsScope.ProfileGroupImpl(
Text(currentInfo?.id.toString())
}
},
- description = { Text("用户 ID") },
+ description = { Text(userIdText) },
modifier = Modifier.placeholder(isPlaceholder),
)
- Group(title = { Text("第三方账号") }) {
+ Group(title = { Text(thirdPartyAccountsText) }) {
TextItem(
title = { Text("Bangumi") },
- description = { Text(currentInfo?.bangumiUsername ?: "未绑定") },
+ description = { Text(currentInfo?.bangumiUsername ?: notBoundText) },
icon = {
Image(Icons.Default.BangumiNext, contentDescription = "Bangumi Icon")
},
onClick = onBangumiClick,
action = if (!currentInfo?.bangumiUsername.isNullOrEmpty()) {
{
- TextButton(onClick = { showUnbindBangumiDialog = true }) { Text("解绑") }
+ TextButton(onClick = { showUnbindBangumiDialog = true }) { Text(unbindText) }
}
} else null,
modifier = Modifier.placeholder(isPlaceholder),
@@ -317,15 +351,15 @@ private fun UnbindBangumiDialog(
AlertDialog(
onCancel,
// icon omitted to reduce dependency on specific icon packs
- text = { Text("确定要解绑 Bangumi 吗?解绑后将不再同步观看记录到 Bangumi。解绑后可以重新绑定。") },
+ text = { Text(stringResource(Lang.settings_account_profile_unbind_bangumi_confirmation)) },
confirmButton = {
TextButton(onConfirm, enabled = confirmEnabled) {
- Text("解绑", color = MaterialTheme.colorScheme.error)
+ Text(stringResource(Lang.settings_account_profile_unbind), color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onCancel) {
- Text("取消")
+ Text(stringResource(Lang.subject_collection_cancel))
}
},
)
@@ -343,9 +377,16 @@ private fun SettingsScope.UploadAvatarDialog(
var filePickerLaunched by rememberSaveable { mutableStateOf(false) }
var cropTarget by remember { mutableStateOf(null) }
val asyncHandler = rememberAsyncHandler()
+ val selectAvatarText = stringResource(Lang.settings_account_profile_select_avatar)
+ val doneText = stringResource(Lang.settings_account_profile_done)
+ val uploadAvatarText = stringResource(Lang.settings_account_profile_upload_avatar)
+ val selectFileText = stringResource(Lang.settings_account_profile_select_file)
+ val selectFileDescriptionText = stringResource(Lang.settings_account_profile_select_file_description)
+ val selectFileDescriptionDesktopText =
+ stringResource(Lang.settings_account_profile_select_file_description_desktop)
val filePicker = rememberFilePickerLauncher(
type = FileKitType.Image,
- title = "选择头像",
+ title = selectAvatarText,
) {
filePickerLaunched = false
it?.let { file ->
@@ -381,26 +422,19 @@ private fun SettingsScope.UploadAvatarDialog(
onClick = onDismissRequest,
enabled = !filePickerLaunched,
) {
- Text("完成")
+ Text(doneText)
}
},
title = {
- Text("上传头像")
+ Text(uploadAvatarText)
},
text = {
Column(modifier) {
+ val selectFileDescription =
+ if (currentPlatform() is Platform.Desktop) selectFileDescriptionDesktopText else selectFileDescriptionText
TextItem(
- title = { Text("选择文件") },
- description = {
- Text(
- buildString {
- if (currentPlatform() is Platform.Desktop) {
- append("或拖动文件到此处。")
- }
- append("支持 JPEG/PNG/WebP,最大 1MB。多次上传需间隔一分钟。")
- },
- )
- },
+ title = { Text(selectFileText) },
+ description = { Text(selectFileDescription) },
onClickEnabled = !filePickerLaunched,
modifier = Modifier
.border(
@@ -479,13 +513,14 @@ private fun SettingsScope.UploadAvatarDialog(
}
}
+@Composable
private fun renderAvatarUploadMessage(
state: EditProfileState.UploadAvatarState,
): String {
return when (state) {
- is EditProfileState.UploadAvatarState.Uploading -> "正在上传..."
- is EditProfileState.UploadAvatarState.SizeExceeded -> "图片大小超过 1MB"
- is EditProfileState.UploadAvatarState.InvalidFormat -> "图片格式不支持"
+ is EditProfileState.UploadAvatarState.Uploading -> stringResource(Lang.settings_account_profile_uploading_avatar)
+ is EditProfileState.UploadAvatarState.SizeExceeded -> stringResource(Lang.settings_account_profile_avatar_size_exceeded)
+ is EditProfileState.UploadAvatarState.InvalidFormat -> stringResource(Lang.settings_account_profile_avatar_invalid_format)
is EditProfileState.UploadAvatarState.UnknownError -> renderLoadErrorMessage(state.loadError)
is EditProfileState.UploadAvatarState.UnknownErrorWithRetry -> renderLoadErrorMessage(state.loadError)
is EditProfileState.UploadAvatarState.Success, EditProfileState.UploadAvatarState.Default -> ""
@@ -537,13 +572,13 @@ private fun CropAvatarDialog(
onConfirmCropped(bytes)
},
) {
- Text("裁剪并上传")
+ Text(stringResource(Lang.settings_account_profile_crop_and_upload))
}
},
dismissButton = {
- TextButton(onDismissRequest) { Text("取消") }
+ TextButton(onDismissRequest) { Text(stringResource(Lang.subject_collection_cancel)) }
},
- title = { Text("裁剪头像") },
+ title = { Text(stringResource(Lang.settings_account_profile_crop_avatar)) },
text = {
Column(Modifier.fillMaxWidth()) {
val isAndroid = currentPlatform() is Platform.Mobile
@@ -776,7 +811,7 @@ private fun CropAvatarDialog(
drawHandle(sx + ss - handlePx / 2, sy + ss - handlePx / 2)
}
- Text("拖动选框移动,拖动角点调整大小", Modifier.padding(top = 8.dp))
+ Text(stringResource(Lang.settings_account_profile_crop_hint), Modifier.padding(top = 8.dp))
}
},
)
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfilePopup.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfilePopup.kt
index efd730e702..dcbf7ecadc 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfilePopup.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfilePopup.kt
@@ -181,4 +181,3 @@ fun AccountLogoutDialog(
},
)
}
-
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfilePopupLayout.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfilePopupLayout.kt
index a83424fc50..aadb9adaca 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfilePopupLayout.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/ProfilePopupLayout.kt
@@ -73,6 +73,11 @@ internal fun ProfilePopupLayout(
modifier: Modifier = Modifier,
) {
val isLogin = remember(state) { state.selfInfo.isSessionValid == true }
+ val notLoggedInText = stringResource(Lang.settings_account_popup_not_logged_in)
+ val editProfileText = stringResource(Lang.settings_account_popup_edit_profile)
+ val loginRegisterText = stringResource(Lang.settings_account_popup_login_register)
+ val settingsText = stringResource(Lang.settings)
+ val logoutText = stringResource(Lang.settings_account_popup_logout)
Column(modifier) {
Box(
modifier = Modifier
@@ -95,7 +100,7 @@ internal fun ProfilePopupLayout(
val showEmail = false
Text(
- if (isLogin) title else stringResource(Lang.settings_account_popup_not_logged_in),
+ if (isLogin) title else notLoggedInText,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
@@ -132,25 +137,25 @@ internal fun ProfilePopupLayout(
Column {
if (isLogin) {
TextItem(
- icon = { Icon(Icons.Outlined.Edit, contentDescription = "Edit profile settings") },
+ icon = { Icon(Icons.Outlined.Edit, contentDescription = editProfileText) },
onClick = onClickEditProfile,
) {
- Text(stringResource(Lang.settings_account_popup_edit_profile))
+ Text(editProfileText)
}
} else {
TextItem(
- icon = { Icon(Icons.AutoMirrored.Outlined.Login, contentDescription = "Login") },
+ icon = { Icon(Icons.AutoMirrored.Outlined.Login, contentDescription = loginRegisterText) },
onClick = onClickLogin,
) {
- Text(stringResource(Lang.settings_account_popup_login_register))
+ Text(loginRegisterText)
}
}
TextItem(
- icon = { Icon(Icons.Outlined.Settings, contentDescription = "Settings") },
+ icon = { Icon(Icons.Outlined.Settings, contentDescription = settingsText) },
onClick = onClickSettings,
) {
- Text(stringResource(Lang.settings))
+ Text(settingsText)
}
if (isLogin) {
@@ -159,14 +164,14 @@ internal fun ProfilePopupLayout(
ProvideContentColor(MaterialTheme.colorScheme.error) {
Icon(
Icons.AutoMirrored.Outlined.Logout,
- contentDescription = "Logout",
+ contentDescription = logoutText,
)
}
},
onClick = onClickLogout,
) {
ProvideContentColor(MaterialTheme.colorScheme.error) {
- Text(stringResource(Lang.settings_account_popup_logout))
+ Text(logoutText)
}
}
}
@@ -242,4 +247,4 @@ private fun PreviewAccountSettingsPopupLayout() {
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/SelfInfoBanner.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/SelfInfoBanner.kt
index 94223d39ce..42e44f5728 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/SelfInfoBanner.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/account/SelfInfoBanner.kt
@@ -34,7 +34,7 @@ import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
import me.him188.ani.app.ui.foundation.avatar.AvatarImage
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
import me.him188.ani.app.ui.lang.Lang
-import me.him188.ani.app.ui.lang.settings_account_popup_login_register
+import me.him188.ani.app.ui.lang.settings_account_login_register
import me.him188.ani.app.ui.user.SelfInfoUiState
import me.him188.ani.app.ui.user.TestSelfInfoUiState
import me.him188.ani.utils.platform.annotations.TestOnly
@@ -53,6 +53,7 @@ internal fun SelfInfoBanner(
containerColor: Color = MaterialTheme.colorScheme.surface
) {
val isLogin = remember(state) { state.isSessionValid == true }
+ val loginRegisterText = stringResource(Lang.settings_account_login_register)
Surface(
checked = checked,
@@ -64,7 +65,7 @@ internal fun SelfInfoBanner(
Row(Modifier.padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = CenterVertically) {
if (!isLogin) {
FilledTonalButton(onLoginClick, Modifier.fillMaxWidth()) {
- Text(stringResource(Lang.settings_account_popup_login_register))
+ Text(loginRegisterText)
}
} else {
AvatarImage(
@@ -117,4 +118,4 @@ private fun PreviewSelfInfoBanner() = ProvideCompositionLocalsForPreview {
{},
)
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/SingleSelectionItem.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/SingleSelectionItem.kt
index 3bbbea125e..40e03c9930 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/SingleSelectionItem.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/SingleSelectionItem.kt
@@ -36,6 +36,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_danmaku_cancel
+import me.him188.ani.app.ui.lang.settings_danmaku_confirm
+import org.jetbrains.compose.resources.stringResource
@Stable
class SingleSelectionElement(
@@ -150,11 +154,11 @@ fun SettingsScope.SingleSelectionItem(
showDialog = false
onConfirm(items.getOrNull(selectionState.currentSelected)?.value)
},
- ) { Text("确认") }
+ ) { Text(stringResource(Lang.settings_danmaku_confirm)) }
},
dismissButton = {
TextButton({ showDialog = false }) {
- Text("取消")
+ Text(stringResource(Lang.settings_danmaku_cancel))
}
},
text = {
@@ -204,4 +208,4 @@ fun SettingsScope.SingleSelectionItem(
},
)
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/SorterItem.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/SorterItem.kt
index d31dd78a47..4ee8154479 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/SorterItem.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/SorterItem.kt
@@ -33,11 +33,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_danmaku_cancel
+import me.him188.ani.app.ui.lang.settings_media_source_save_button
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.detectReorder
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable
+import org.jetbrains.compose.resources.stringResource
@Stable
class SelectableItem(
@@ -156,12 +160,12 @@ fun SettingsScope.SorterItem(
onSort(sortingData)
},
) {
- Text("保存")
+ Text(stringResource(Lang.settings_media_source_save_button))
}
},
dismissButton = {
TextButton({ showDialog = false }) {
- Text("取消")
+ Text(stringResource(Lang.settings_danmaku_cancel))
}
},
)
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/TextFieldItem.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/TextFieldItem.kt
index bfc1a48829..7db71e6420 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/TextFieldItem.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/framework/components/TextFieldItem.kt
@@ -48,7 +48,10 @@ import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.foundation.effects.defaultFocus
import me.him188.ani.app.ui.foundation.effects.onKey
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_danmaku_cancel
import me.him188.ani.app.ui.settings.SettingsTab
+import org.jetbrains.compose.resources.stringResource
/**
@@ -236,7 +239,7 @@ internal fun SettingsScope.TextFieldDialog(
}
},
dismissButton = {
- TextButton(onClick = onDismissRequest) { Text("取消") }
+ TextButton(onClick = onDismissRequest) { Text(stringResource(Lang.settings_danmaku_cancel)) }
},
)
}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/detail/RssDetailPane.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/detail/RssDetailPane.kt
index 5571f9a1ba..49def4f6c2 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/detail/RssDetailPane.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/detail/RssDetailPane.kt
@@ -70,8 +70,9 @@ import me.him188.ani.app.ui.lang.settings_mediasource_rss_publish_time
import me.him188.ani.app.ui.lang.settings_mediasource_rss_resolution
import me.him188.ani.app.ui.lang.settings_mediasource_rss_subtitle_language
import me.him188.ani.app.ui.lang.settings_mediasource_rss_unknown
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.settings.mediasource.rss.test.TestRssItemInfos
-import me.him188.ani.app.ui.settings.mediasource.rss.test.subtitleLanguageRendered
+import me.him188.ani.app.ui.settings.mediasource.rss.test.renderSubtitleLanguageRendered
import me.him188.ani.datasources.api.Media
import me.him188.ani.datasources.api.topic.isSingleEpisode
import me.him188.ani.utils.platform.annotations.TestOnly
@@ -135,6 +136,7 @@ private fun RssItemDetailColumn(
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
val toaster = LocalToaster.current
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
// Load string resources in composable context
val copiedText = stringResource(Lang.settings_mediasource_rss_copied)
@@ -212,7 +214,7 @@ private fun RssItemDetailColumn(
ListItem(
headlineContent = { Text(stringResource(Lang.settings_mediasource_rss_subtitle_language)) },
leadingContent = { Icon(Icons.Rounded.Subtitles, contentDescription = null) },
- supportingContent = { SelectionContainer { Text(item.subtitleLanguageRendered) } },
+ supportingContent = { SelectionContainer { Text(item.renderSubtitleLanguageRendered(mediaDetailsStrings)) } },
)
}
item {
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/FinalResultTab.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/FinalResultTab.kt
index 3f7be16255..2a1c28b418 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/FinalResultTab.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/FinalResultTab.kt
@@ -39,6 +39,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import me.him188.ani.app.tools.formatDateTime
+import me.him188.ani.app.ui.media.MediaDetailsStrings
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.media.renderSubtitleLanguage
import me.him188.ani.datasources.api.Media
import me.him188.ani.datasources.api.topic.FileSize
@@ -56,6 +58,7 @@ fun RssTestPaneDefaults.FinalResultTab(
val selectedItem by remember(selectedItemProvider) {
derivedStateOf(selectedItemProvider)
}
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
LazyVerticalStaggeredGrid(
StaggeredGridCells.Adaptive(minSize = 300.dp),
modifier,
@@ -66,6 +69,7 @@ fun RssTestPaneDefaults.FinalResultTab(
items(result.mediaList, key = { "Rss-FinalResultTab-" + it.mediaId }) { item ->
RssTestResultMediaItem(
item,
+ mediaDetailsStrings = mediaDetailsStrings,
isSelected = selectedItem == item,
onClick = {
onViewDetails(item)
@@ -79,6 +83,7 @@ fun RssTestPaneDefaults.FinalResultTab(
@Composable
fun RssTestResultMediaItem(
media: Media,
+ mediaDetailsStrings: MediaDetailsStrings,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
@@ -119,7 +124,7 @@ fun RssTestResultMediaItem(
InputChip(
false,
onClick = { },
- label = { Text(renderSubtitleLanguage(it)) },
+ label = { Text(renderSubtitleLanguage(it, mediaDetailsStrings)) },
)
}
}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/OverviewTab.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/OverviewTab.kt
index 72390a294b..7ce1608ec3 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/OverviewTab.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/OverviewTab.kt
@@ -53,6 +53,7 @@ import me.him188.ani.app.ui.foundation.interaction.onRightClickIfSupported
import me.him188.ani.app.ui.foundation.setClipEntryText
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_mediasource_copy
import me.him188.ani.app.ui.lang.settings_mediasource_rss_copied_to_clipboard
import me.him188.ani.datasources.api.topic.titles.ParsedTopicTitle
import me.him188.ani.utils.platform.annotations.TestOnly
@@ -93,6 +94,7 @@ fun RssOverviewCard(
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
val textCopied = stringResource(Lang.settings_mediasource_rss_copied_to_clipboard)
+ val copyText = stringResource(Lang.settings_mediasource_copy)
val copy = { str: String ->
scope.launch {
clipboard.setClipEntryText(str)
@@ -104,9 +106,9 @@ fun RssOverviewCard(
val func: () -> Unit = { copy(value()) }
return combinedClickable(
onLongClick = func,
- onLongClickLabel = "复制",
+ onLongClickLabel = copyText,
onClick = func, // no-op
- onClickLabel = "复制",
+ onClickLabel = copyText,
).onRightClickIfSupported(onClick = func)
}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/RssInfoTab.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/RssInfoTab.kt
index 3f8b04754c..f0f7041193 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/RssInfoTab.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/RssInfoTab.kt
@@ -38,13 +38,16 @@ import me.him188.ani.app.domain.mediasource.test.rss.RssItemInfo
import me.him188.ani.app.tools.formatDateTime
import me.him188.ani.app.ui.foundation.OutlinedTag
import me.him188.ani.app.ui.media.MediaDetailsRenderer
+import me.him188.ani.app.ui.media.MediaDetailsStrings
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.utils.platform.annotations.TestOnly
@Stable
-val RssItemInfo.subtitleLanguageRendered: String
- get() = MediaDetailsRenderer.renderSubtitleLanguages(
+fun RssItemInfo.renderSubtitleLanguageRendered(strings: MediaDetailsStrings): String =
+ MediaDetailsRenderer.renderSubtitleLanguages(
parsed.subtitleKind,
- parsed.subtitleLanguages.map { it.displayName },
+ parsed.subtitleLanguages,
+ strings,
)
@Composable
@@ -59,6 +62,7 @@ fun RssTestPaneDefaults.RssInfoTab(
val selectedItem by remember(selectedItemProvider) {
derivedStateOf(selectedItemProvider)
}
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
LazyVerticalStaggeredGrid(
StaggeredGridCells.Adaptive(minSize = 300.dp),
modifier,
@@ -69,6 +73,7 @@ fun RssTestPaneDefaults.RssInfoTab(
items(items) { item ->
RssTestResultRssItem(
item,
+ mediaDetailsStrings = mediaDetailsStrings,
isSelected = selectedItem == item,
onClick = {
onViewDetails(item)
@@ -82,6 +87,7 @@ fun RssTestPaneDefaults.RssInfoTab(
@Composable
fun RssTestResultRssItem(
item: RssItemInfo,
+ mediaDetailsStrings: MediaDetailsStrings,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
@@ -106,6 +112,7 @@ fun RssTestResultRssItem(
for (tag in item.tags) {
OutlinedMatchTag(tag)
}
+ OutlinedTag { Text(item.renderSubtitleLanguageRendered(mediaDetailsStrings)) }
item.rss.pubDate?.let {
OutlinedTag { Text(formatDateTime(it)) }
}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/RssTestPane.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/RssTestPane.kt
index b9c6550033..f737b273de 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/RssTestPane.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/rss/test/RssTestPane.kt
@@ -47,6 +47,7 @@ import me.him188.ani.app.ui.foundation.interaction.nestedScrollWorkaround
import me.him188.ani.app.ui.foundation.layout.connectedScroll
import me.him188.ani.app.ui.foundation.layout.rememberConnectedScrollState
import me.him188.ani.app.ui.foundation.widgets.FastLinearProgressIndicator
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.settings.mediasource.EditMediaSourceTestDataCardDefaults
import me.him188.ani.app.ui.settings.mediasource.RefreshIndicatedHeadlineRow
import me.him188.ani.app.ui.settings.mediasource.rss.detail.RssViewingItem
@@ -227,9 +228,11 @@ object RssTestPaneDefaults
@Composable
@PreviewLightDark
fun PreviewRssTestResultItem() {
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
RssTestResultMediaItem(
- TestMediaList[0],
- false,
- {},
+ media = TestMediaList[0],
+ mediaDetailsStrings = mediaDetailsStrings,
+ isSelected = false,
+ onClick = {},
)
}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigPane.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigPane.kt
index 837cb50bdd..53624cf16a 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigPane.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/edit/SelectorConfigPane.kt
@@ -68,11 +68,51 @@ import me.him188.ani.app.ui.foundation.effects.moveFocusOnEnter
import me.him188.ani.app.ui.foundation.stateOf
import me.him188.ani.app.ui.foundation.text.ProvideTextStyleContentColor
import me.him188.ani.app.ui.foundation.theme.EasingDurations
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_mediasource_rss_auto_save_hint
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_base_url
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_base_url_supporting
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_channel_format_grouped
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_channel_format_no_channel
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_default_resolution
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_default_resolution_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_default_subtitle_language
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_default_subtitle_language_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_distinguish_channel_name
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_distinguish_channel_name_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_distinguish_subject_name
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_distinguish_subject_name_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_filter_by_episode_sort
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_filter_by_episode_sort_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_filter_by_subject_name
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_filter_by_subject_name_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_filter_settings
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_icon_url
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_name
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_name_placeholder
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_player_select_resource
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_referer_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_request_interval
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_request_interval_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_first_word
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_first_word_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_remove_special
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_remove_special_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_subject_names_count
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_subject_names_count_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_url
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_url_placeholder
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_search_url_supporting
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_subject_format_multi_tag
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_subject_format_single_tag
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_user_agent_description
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_config_video_playback
import me.him188.ani.app.ui.settings.mediasource.rss.createTestSaveableStorage
import me.him188.ani.app.ui.settings.mediasource.rss.edit.MediaSourceHeadline
import me.him188.ani.datasources.api.topic.Resolution
import me.him188.ani.datasources.api.topic.SubtitleLanguage
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
import kotlin.time.Duration.Companion.milliseconds
@Composable
@@ -105,8 +145,8 @@ internal fun SelectorConfigurationPane(
Modifier
.fillMaxWidth()
.moveFocusOnEnter(),
- label = { Text("名称*") },
- placeholder = { Text("设置显示在列表中的名称") },
+ label = { Text(stringResource(Lang.settings_mediasource_selector_config_name)) },
+ placeholder = { Text(stringResource(Lang.settings_mediasource_selector_config_name_placeholder)) },
isError = state.displayNameIsError,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
shape = textFieldShape,
@@ -117,7 +157,7 @@ internal fun SelectorConfigurationPane(
Modifier
.fillMaxWidth()
.moveFocusOnEnter(),
- label = { Text("图标链接") },
+ label = { Text(stringResource(Lang.settings_mediasource_selector_config_icon_url)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
shape = textFieldShape,
enabled = state.enableEdit,
@@ -137,20 +177,15 @@ internal fun SelectorConfigurationPane(
OutlinedTextField(
state.searchUrl, { state.searchUrl = it },
Modifier.fillMaxWidth().moveFocusOnEnter(),
- label = { Text("搜索链接") },
+ label = { Text(stringResource(Lang.settings_mediasource_selector_config_search_url)) },
placeholder = {
Text(
- "示例:https://www.nyacg.net/search.html?wd={keyword}",
+ stringResource(Lang.settings_mediasource_selector_config_search_url_placeholder),
color = MaterialTheme.colorScheme.outline,
)
},
supportingText = {
- Text(
- """
- 替换规则:
- {keyword} 替换为条目 (番剧) 名称
- """.trimIndent(),
- )
+ Text(stringResource(Lang.settings_mediasource_selector_config_search_url_supporting))
},
isError = state.searchUrlIsError,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
@@ -162,16 +197,14 @@ internal fun SelectorConfigurationPane(
Modifier
.padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp))
.fillMaxWidth().moveFocusOnEnter(),
- label = { Text("Base URL (可选)") },
+ label = { Text(stringResource(Lang.settings_mediasource_selector_config_base_url)) },
placeholder = state.baseUrlPlaceholder?.let {
{
Text(it, color = MaterialTheme.colorScheme.outline)
}
},
supportingText = {
- Text(
- """可选。用于拼接条目详情 (剧集列表) 页面 URL,将会影响步骤 2。默认自动从搜索链接生成""".trimIndent(),
- )
+ Text(stringResource(Lang.settings_mediasource_selector_config_base_url_supporting))
},
isError = state.searchUrlIsError,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
@@ -179,13 +212,15 @@ internal fun SelectorConfigurationPane(
enabled = state.enableEdit,
)
ListItem(
- headlineContent = { Text("仅使用第一个词") },
+ headlineContent = { Text(stringResource(Lang.settings_mediasource_selector_config_search_first_word)) },
Modifier
.padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp))
.clickable(enabled = state.enableEdit) {
state.searchUseOnlyFirstWord = !state.searchUseOnlyFirstWord
},
- supportingContent = { Text("以空格分割,仅使用第一个词搜索。适用于搜索兼容性差的情况") },
+ supportingContent = {
+ Text(stringResource(Lang.settings_mediasource_selector_config_search_first_word_description))
+ },
trailingContent = {
Switch(
state.searchUseOnlyFirstWord, { state.searchUseOnlyFirstWord = it },
@@ -195,13 +230,15 @@ internal fun SelectorConfigurationPane(
colors = listItemColors,
)
ListItem(
- headlineContent = { Text("去除特殊字符") },
+ headlineContent = { Text(stringResource(Lang.settings_mediasource_selector_config_search_remove_special)) },
Modifier
.padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp))
.clickable(enabled = state.enableEdit) {
state.searchRemoveSpecial = !state.searchRemoveSpecial
},
- supportingContent = { Text("去除特殊字符以及 \"电影\" 等字样,提升搜索成功率") },
+ supportingContent = {
+ Text(stringResource(Lang.settings_mediasource_selector_config_search_remove_special_description))
+ },
trailingContent = {
Switch(
state.searchRemoveSpecial, { state.searchRemoveSpecial = it },
@@ -223,15 +260,9 @@ internal fun SelectorConfigurationPane(
Modifier
.padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp))
.fillMaxWidth().moveFocusOnEnter(),
- label = { Text("尝试条目名称数量") },
+ label = { Text(stringResource(Lang.settings_mediasource_selector_config_search_subject_names_count)) },
supportingText = {
- Text(
- """
- 每次播放使用多少个条目名称进行查询。
- 为 1 则只使用主中文名称,为 2 额外使用日文原名,大于 2 将额外使用其他别名,别名的数量不固定。
- 一般用 1 就够了,使用多个名称将会显著增加播放时的等待时间。
- """.trimIndent(),
- )
+ Text(stringResource(Lang.settings_mediasource_selector_config_search_subject_names_count_description))
},
isError = searchUseSubjectNamesCount.toIntOrNull().let {
it == null || it < 1
@@ -252,11 +283,9 @@ internal fun SelectorConfigurationPane(
Modifier
.padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp))
.fillMaxWidth().moveFocusOnEnter(),
- label = { Text("搜索请求间隔时间 (毫秒)") },
+ label = { Text(stringResource(Lang.settings_mediasource_selector_config_request_interval)) },
supportingText = {
- Text(
- """控制每发送一个请求后等待多久后再发送下一个请求""".trimIndent(),
- )
+ Text(stringResource(Lang.settings_mediasource_selector_config_request_interval_description))
},
isError = requestIntervalString.toLongOrNull() == null,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
@@ -317,7 +346,7 @@ internal fun SelectorConfigurationPane(
MaterialTheme.typography.titleMedium,
MaterialTheme.colorScheme.primary,
) {
- Text("过滤设置")
+ Text(stringResource(Lang.settings_mediasource_selector_config_filter_settings))
}
}
@@ -326,11 +355,13 @@ internal fun SelectorConfigurationPane(
verticalArrangement = Arrangement.spacedBy((verticalSpacing - 16.dp).coerceAtLeast(0.dp)),
) {
ListItem(
- headlineContent = { Text("使用条目名称过滤") },
+ headlineContent = { Text(stringResource(Lang.settings_mediasource_selector_config_filter_by_subject_name)) },
Modifier.focusable(false).clickable(
enabled = state.enableEdit,
) { state.filterBySubjectName = !state.filterBySubjectName },
- supportingContent = { Text("要求资源标题包含条目名称。适用于数据源可能搜到无关内容的情况。此功能只对 4.4.0 以前版本有效,对其他版本无效") },
+ supportingContent = {
+ Text(stringResource(Lang.settings_mediasource_selector_config_filter_by_subject_name_description))
+ },
trailingContent = {
Switch(
state.filterBySubjectName, { state.filterBySubjectName = it },
@@ -340,11 +371,13 @@ internal fun SelectorConfigurationPane(
colors = listItemColors,
)
ListItem(
- headlineContent = { Text("使用剧集序号过滤") },
+ headlineContent = { Text(stringResource(Lang.settings_mediasource_selector_config_filter_by_episode_sort)) },
Modifier.focusable(false).clickable(
enabled = state.enableEdit,
) { state.filterByEpisodeSort = !state.filterByEpisodeSort },
- supportingContent = { Text("要求资源标题包含剧集序号。适用于数据源可能搜到无关内容的情况。通常建议开启") },
+ supportingContent = {
+ Text(stringResource(Lang.settings_mediasource_selector_config_filter_by_episode_sort_description))
+ },
trailingContent = {
Switch(
state.filterByEpisodeSort, { state.filterByEpisodeSort = it },
@@ -373,11 +406,13 @@ internal fun SelectorConfigurationPane(
kotlin.run {
var showMenu by rememberSaveable { mutableStateOf(false) }
ListItem(
- headlineContent = { Text("标记分辨率") },
+ headlineContent = { Text(stringResource(Lang.settings_mediasource_selector_config_default_resolution)) },
Modifier.focusable(false).clickable(
enabled = state.enableEdit,
) { showMenu = !showMenu },
- supportingContent = { Text("将此数据源的资源都标记为该分辨率。不影响查询,只在播放器中选择数据源时用做偏好和过滤选项。") },
+ supportingContent = {
+ Text(stringResource(Lang.settings_mediasource_selector_config_default_resolution_description))
+ },
trailingContent = {
TextButton(onClick = { showMenu = true }) {
Text(state.defaultResolution.displayName)
@@ -403,11 +438,13 @@ internal fun SelectorConfigurationPane(
kotlin.run {
var showMenu by rememberSaveable { mutableStateOf(false) }
ListItem(
- headlineContent = { Text("标记字幕语言") },
+ headlineContent = { Text(stringResource(Lang.settings_mediasource_selector_config_default_subtitle_language)) },
Modifier.focusable(false).clickable(
enabled = state.enableEdit,
) { showMenu = !showMenu },
- supportingContent = { Text("将此数据源的资源都标记为该字幕语言。不影响查询,只在播放器中选择数据源时用做偏好和过滤选项。") },
+ supportingContent = {
+ Text(stringResource(Lang.settings_mediasource_selector_config_default_subtitle_language_description))
+ },
trailingContent = {
TextButton(onClick = { showMenu = true }) {
Text(state.defaultSubtitleLanguage.displayName)
@@ -435,23 +472,19 @@ internal fun SelectorConfigurationPane(
MaterialTheme.typography.titleMedium,
MaterialTheme.colorScheme.primary,
) {
- Text("在播放器内选择资源时")
+ Text(stringResource(Lang.settings_mediasource_selector_config_player_select_resource))
}
}
Column(Modifier, verticalArrangement = Arrangement.spacedBy(verticalSpacing)) {
val conf = state.selectMediaConfig
ListItem(
- headlineContent = { Text("区分条目名称") },
+ headlineContent = { Text(stringResource(Lang.settings_mediasource_selector_config_distinguish_subject_name)) },
Modifier.focusable(false).clickable(
enabled = state.enableEdit,
) { conf.distinguishSubjectName = !conf.distinguishSubjectName },
supportingContent = {
- Text(
- "关闭后,所有步骤 1 搜索到的条目都将被视为同一个,它们的相同标题的剧集将会被去重。" +
- "开启此项则不会这样去重。\n" +
- "此选项不影响测试结果,影响播放器内选择数据源时的结果。",
- )
+ Text(stringResource(Lang.settings_mediasource_selector_config_distinguish_subject_name_description))
},
trailingContent = {
Switch(
@@ -462,16 +495,12 @@ internal fun SelectorConfigurationPane(
colors = listItemColors,
)
ListItem(
- headlineContent = { Text("区分线路名称") },
+ headlineContent = { Text(stringResource(Lang.settings_mediasource_selector_config_distinguish_channel_name)) },
Modifier.focusable(false).clickable(
enabled = state.enableEdit,
) { conf.distinguishChannelName = !conf.distinguishChannelName },
supportingContent = {
- Text(
- "关闭后,线路名称不同,但只要标题相同的剧集就会被去重。" +
- "开启此项则不会这样去重。\n" +
- "此选项不影响测试结果,影响播放器内选择数据源时的结果。",
- )
+ Text(stringResource(Lang.settings_mediasource_selector_config_distinguish_channel_name_description))
},
trailingContent = {
Switch(
@@ -488,7 +517,7 @@ internal fun SelectorConfigurationPane(
MaterialTheme.typography.titleMedium,
MaterialTheme.colorScheme.primary,
) {
- Text("播放视频时")
+ Text(stringResource(Lang.settings_mediasource_selector_config_video_playback))
}
}
@@ -498,7 +527,7 @@ internal fun SelectorConfigurationPane(
conf.referer, { conf.referer = it },
Modifier.fillMaxWidth().moveFocusOnEnter(),
label = { Text("Referer") },
- supportingText = { Text("播放视频时执行的 HTTP 请求的 Referer,可留空") },
+ supportingText = { Text(stringResource(Lang.settings_mediasource_selector_config_referer_description)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
shape = textFieldShape,
enabled = state.enableEdit,
@@ -507,7 +536,7 @@ internal fun SelectorConfigurationPane(
conf.userAgent, { conf.userAgent = it },
Modifier.fillMaxWidth().moveFocusOnEnter(),
label = { Text("User-Agent") },
- supportingText = { Text("播放视频时执行的 HTTP 请求的 User-Agent") },
+ supportingText = { Text(stringResource(Lang.settings_mediasource_selector_config_user_agent_description)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
shape = textFieldShape,
enabled = state.enableEdit,
@@ -520,7 +549,7 @@ internal fun SelectorConfigurationPane(
MaterialTheme.typography.labelMedium,
MaterialTheme.colorScheme.outline,
) {
- Text("提示:修改自动保存")
+ Text(stringResource(Lang.settings_mediasource_rss_auto_save_hint))
}
}
}
@@ -556,8 +585,8 @@ private fun SelectorSubjectFormatSelectionButtonRow(
Btn(format.id, index) {
Text(
when (format) { // type-safe to handle all formats
- SelectorSubjectFormatA -> "单标签"
- SelectorSubjectFormatIndexed -> "多标签"
+ SelectorSubjectFormatA -> stringResource(Lang.settings_mediasource_selector_config_subject_format_single_tag)
+ SelectorSubjectFormatIndexed -> stringResource(Lang.settings_mediasource_selector_config_subject_format_multi_tag)
SelectorSubjectFormatJsonPathIndexed -> "JsonPath"
},
softWrap = false,
@@ -593,8 +622,8 @@ private fun SelectorChannelSelectionButtonRow(
Btn(selectorChannelFormat.id, index) {
Text(
when (selectorChannelFormat) { // type-safe to handle all formats
- SelectorChannelFormatNoChannel -> "不区分线路"
- SelectorChannelFormatIndexGrouped -> "线路分组"
+ SelectorChannelFormatNoChannel -> stringResource(Lang.settings_mediasource_selector_config_channel_format_no_channel)
+ SelectorChannelFormatIndexGrouped -> stringResource(Lang.settings_mediasource_selector_config_channel_format_grouped)
},
softWrap = false,
)
@@ -618,4 +647,4 @@ fun PreviewSelectorConfigurationPane() = ProvideCompositionLocalsForPreview {
},
)
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/episode/SelectorEpisodePane.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/episode/SelectorEpisodePane.kt
index c66317d5b3..c35038af7a 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/episode/SelectorEpisodePane.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/episode/SelectorEpisodePane.kt
@@ -84,6 +84,19 @@ import me.him188.ani.app.ui.foundation.stateOf
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
import me.him188.ani.app.ui.foundation.widgets.FastLinearProgressIndicator
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_mediasource_copied
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_actual_play_url
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_hide_css
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_hide_data
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_hide_images
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_hide_scripts
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_matched
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_multiple_matched_video
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_nested_link
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_no_matched_video
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_not_matched
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_single_matched_video
import me.him188.ani.app.ui.settings.mediasource.rss.createTestSaveableStorage
import me.him188.ani.app.ui.settings.mediasource.selector.EditSelectorMediaSourcePageState
import me.him188.ani.app.ui.settings.mediasource.selector.edit.SelectorConfigurationDefaults
@@ -91,6 +104,7 @@ import me.him188.ani.app.ui.settings.mediasource.selector.test.SelectorTestPane
import me.him188.ani.app.ui.settings.mediasource.selector.test.TestSelectorMediaSourceEngine
import me.him188.ani.datasources.api.EpisodeSort
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
import kotlin.coroutines.EmptyCoroutineContext
@Composable
@@ -213,6 +227,20 @@ fun SelectorEpisodePaneContent(
list.count { it.isMatchedVideo() }
}
}
+ val noMatchedVideoText = stringResource(
+ Lang.settings_mediasource_selector_episode_no_matched_video,
+ list.size,
+ )
+ val singleMatchedVideoText = stringResource(
+ Lang.settings_mediasource_selector_episode_single_matched_video,
+ list.size,
+ matchedVideoSize,
+ )
+ val multipleMatchedVideoText = stringResource(
+ Lang.settings_mediasource_selector_episode_multiple_matched_video,
+ list.size,
+ matchedVideoSize,
+ )
ProvideTextStyle(MaterialTheme.typography.titleMedium) {
when (matchedVideoSize) {
0 -> {
@@ -221,7 +249,7 @@ fun SelectorEpisodePaneContent(
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
- Text("根据步骤 3 的配置,从 ${list.size} 个链接中未匹配到播放链接,请检查配置")
+ Text(noMatchedVideoText)
}
1 -> {
@@ -230,7 +258,7 @@ fun SelectorEpisodePaneContent(
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
- Text("根据步骤 3 的配置,从 ${list.size} 个链接中匹配到了 $matchedVideoSize 个链接")
+ Text(singleMatchedVideoText)
}
else -> {
@@ -239,12 +267,16 @@ fun SelectorEpisodePaneContent(
contentDescription = null,
tint = Color.Yellow.compositeOver(MaterialTheme.colorScheme.error),
)
- Text("根据步骤 3 的配置,从 ${list.size} 个链接中匹配到了 $matchedVideoSize 个链接。为了更好的稳定性,建议调整规则,匹配到正好一个链接")
+ Text(multipleMatchedVideoText)
}
}
}
}
+ val hideImagesText = stringResource(Lang.settings_mediasource_selector_episode_hide_images)
+ val hideCssText = stringResource(Lang.settings_mediasource_selector_episode_hide_css)
+ val hideScriptsText = stringResource(Lang.settings_mediasource_selector_episode_hide_scripts)
+ val hideDataText = stringResource(Lang.settings_mediasource_selector_episode_hide_data)
FlowRow(
Modifier.padding(horizontal = horizontalPadding).padding(bottom = 20.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -252,30 +284,34 @@ fun SelectorEpisodePaneContent(
FilterChip(
selected = state.hideImages,
{ state.hideImages = !state.hideImages },
- label = { Text("隐藏图片") },
+ label = { Text(hideImagesText) },
leadingIcon = { if (state.hideImages) Icon(Icons.Rounded.Check, null) },
)
FilterChip(
selected = state.hideCss,
{ state.hideCss = !state.hideCss },
- label = { Text("隐藏 CSS/字体") },
+ label = { Text(hideCssText) },
leadingIcon = { if (state.hideCss) Icon(Icons.Rounded.Check, null) },
)
FilterChip(
selected = state.hideScripts,
{ state.hideScripts = !state.hideScripts },
- label = { Text("隐藏 JS/WASM") },
+ label = { Text(hideScriptsText) },
leadingIcon = { if (state.hideScripts) Icon(Icons.Rounded.Check, null) },
)
FilterChip(
selected = state.hideData,
{ state.hideData = !state.hideData },
- label = { Text("隐藏 data") },
+ label = { Text(hideDataText) },
leadingIcon = { if (state.hideData) Icon(Icons.Rounded.Check, null) },
)
}
val filteredList by state.filteredResults.collectAsStateWithLifecycle(emptyList())
+ val copiedText = stringResource(Lang.settings_mediasource_copied)
+ val nestedLinkText = stringResource(Lang.settings_mediasource_selector_episode_nested_link)
+ val matchedText = stringResource(Lang.settings_mediasource_selector_episode_matched)
+ val notMatchedText = stringResource(Lang.settings_mediasource_selector_episode_not_matched)
LazyColumn(
contentPadding = PaddingValues(
@@ -303,18 +339,23 @@ fun SelectorEpisodePaneContent(
.clickable {
scope.launch {
clipboard.setClipEntryText(matchResult.originalUrl)
- toaster.toast("已复制")
+ toaster.toast(copiedText)
}
},
supportingContent = {
val m3u8 = matchResult.video?.m3u8Url
when {
m3u8 != null && m3u8 != matchResult.originalUrl -> {
- Text("将实际播放:${m3u8}")
+ Text(
+ stringResource(
+ Lang.settings_mediasource_selector_episode_actual_play_url,
+ m3u8,
+ ),
+ )
}
matchResult.webUrl.didLoadNestedPage -> {
- Text("嵌套链接")
+ Text(nestedLinkText)
}
}
},
@@ -323,11 +364,11 @@ fun SelectorEpisodePaneContent(
Column(horizontalAlignment = Alignment.CenterHorizontally) {
when {
matchResult.highlight -> {
- Icon(Icons.Rounded.Check, "匹配", tint = MaterialTheme.colorScheme.primary)
+ Icon(Icons.Rounded.Check, matchedText, tint = MaterialTheme.colorScheme.primary)
}
else -> {
- Icon(Icons.Rounded.Close, "未匹配")
+ Icon(Icons.Rounded.Close, notMatchedText)
}
}
}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/episode/SelectorEpisodePaneDefaults.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/episode/SelectorEpisodePaneDefaults.kt
index cbc4564ba4..2401c153f2 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/episode/SelectorEpisodePaneDefaults.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/mediasource/selector/episode/SelectorEpisodePaneDefaults.kt
@@ -35,10 +35,15 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_edit_config
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_open_link_failed
+import me.him188.ani.app.ui.lang.settings_mediasource_selector_episode_open_original_link
import me.him188.ani.app.ui.settings.mediasource.RefreshIndicationDefaults
import me.him188.ani.app.ui.settings.mediasource.selector.edit.MatchVideoSection
import me.him188.ani.app.ui.settings.mediasource.selector.edit.SelectorConfigState
import me.him188.ani.app.ui.settings.mediasource.selector.edit.SelectorConfigurationDefaults
+import org.jetbrains.compose.resources.stringResource
object SelectorEpisodePaneDefaults {
@Composable
@@ -71,16 +76,21 @@ object SelectorEpisodePaneDefaults {
val uriHandler = LocalUriHandler.current
val toaster = LocalToaster.current
if (state.episodeUrl.isNotBlank() && state.episodeUrl.startsWith("http")) {
+ val openLinkFailedText = stringResource(Lang.settings_mediasource_selector_episode_open_link_failed)
+ val openOriginalLinkText = stringResource(
+ Lang.settings_mediasource_selector_episode_open_original_link,
+ state.episodeName,
+ )
IconButton(
{
try {
uriHandler.openUri(state.episodeUrl)
} catch (e: Throwable) {
- toaster.toast("无法打开链接")
+ toaster.toast(openLinkFailedText)
}
},
) {
- Icon(Icons.Rounded.ArrowOutward, "打开原始链接 ${state.episodeName}")
+ Icon(Icons.Rounded.ArrowOutward, openOriginalLinkText)
}
}
},
@@ -98,12 +108,13 @@ object SelectorEpisodePaneDefaults {
textFieldShape: Shape = SelectorConfigurationDefaults.textFieldShape,
verticalSpacing: Dp = SelectorConfigurationDefaults.verticalSpacing,
) {
+ val editConfigText = stringResource(Lang.settings_mediasource_selector_episode_edit_config)
Column(modifier.padding(contentPadding)) {
Row(Modifier.Companion.padding(bottom = 16.dp)) {
ProvideTextStyle(
MaterialTheme.typography.titleLarge,
) {
- Text("编辑配置")
+ Text(editConfigText)
}
}
SelectorConfigurationDefaults.MatchVideoSection(
@@ -114,4 +125,4 @@ object SelectorEpisodePaneDefaults {
}
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/media/MediaSelectionGroup.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/media/MediaSelectionGroup.kt
index 37c958036f..57c75e461c 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/media/MediaSelectionGroup.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/media/MediaSelectionGroup.kt
@@ -64,17 +64,18 @@ import me.him188.ani.app.ui.lang.settings_media_source_bt
import me.him188.ani.app.ui.lang.settings_media_source_no_preference
import me.him188.ani.app.ui.lang.settings_media_source_web
import me.him188.ani.app.ui.lang.settings_media_subtitle_language
+import me.him188.ani.app.ui.lang.settings_media_video_link_resolve_timeout
+import me.him188.ani.app.ui.lang.settings_media_video_link_resolve_timeout_description
import me.him188.ani.app.ui.lang.settings_media_wait_time_10s
import me.him188.ani.app.ui.lang.settings_media_wait_time_15s
import me.him188.ani.app.ui.lang.settings_media_wait_time_20s
-import me.him188.ani.app.ui.lang.settings_media_wait_time_3s
import me.him188.ani.app.ui.lang.settings_media_wait_time_30s
+import me.him188.ani.app.ui.lang.settings_media_wait_time_3s
import me.him188.ani.app.ui.lang.settings_media_wait_time_5s
import me.him188.ani.app.ui.lang.settings_media_wait_time_8s
import me.him188.ani.app.ui.lang.settings_media_wait_time_infinite
import me.him188.ani.app.ui.lang.settings_media_wait_time_none
-import me.him188.ani.app.ui.lang.settings_media_video_link_resolve_timeout
-import me.him188.ani.app.ui.lang.settings_media_video_link_resolve_timeout_description
+import me.him188.ani.app.ui.media.rememberMediaDetailsStrings
import me.him188.ani.app.ui.media.renderResolution
import me.him188.ani.app.ui.media.renderSubtitleLanguage
import me.him188.ani.app.ui.settings.framework.SettingsState
@@ -131,6 +132,7 @@ class MediaSelectionGroupState(
internal fun SettingsScope.MediaSelectionGroup(
state: MediaSelectionGroupState
) {
+ val mediaDetailsStrings = rememberMediaDetailsStrings()
Group(
title = {
Text(stringResource(Lang.settings_media_preference_title))
@@ -157,20 +159,20 @@ internal fun SettingsScope.MediaSelectionGroup(
},
exposed = { list ->
Text(
- remember(list) {
+ remember(list, mediaDetailsStrings) {
if (list.fastAll { it.selected }) {
textAny
} else if (list.fastAll { !it.selected }) {
textNone
} else
list.asSequence().filter { it.selected }
- .joinToString { renderSubtitleLanguage(it.item) }
+ .joinToString { renderSubtitleLanguage(it.item, mediaDetailsStrings) }
},
softWrap = false,
overflow = TextOverflow.Ellipsis,
)
},
- item = { Text(renderSubtitleLanguage(it)) },
+ item = { Text(renderSubtitleLanguage(it, mediaDetailsStrings)) },
key = { it },
dialogDescription = {
Text(
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/media/torrent/peer/AddBlockedIPDialog.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/media/torrent/peer/AddBlockedIPDialog.kt
index c065ff18cf..530cc66e38 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/media/torrent/peer/AddBlockedIPDialog.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/media/torrent/peer/AddBlockedIPDialog.kt
@@ -50,8 +50,12 @@ import me.him188.ani.app.ui.comment.CommentEditorTextState
import me.him188.ani.app.ui.foundation.LocalPlatform
import me.him188.ani.app.ui.foundation.ifThen
import me.him188.ani.app.ui.foundation.text.ProvideContentColor
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_danmaku_cancel
+import me.him188.ani.app.ui.lang.settings_media_source_subscription_add_confirm
import me.him188.ani.utils.platform.Platform
import me.him188.ani.utils.platform.currentPlatform
+import org.jetbrains.compose.resources.stringResource
private val IPV4_REGEX =
Regex("^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\$")
@@ -157,13 +161,13 @@ fun AddBlockedIPDialog(
enabled = dialogAddButtonEnabled,
onClick = doAdd,
) {
- Text("添加")
+ Text(stringResource(Lang.settings_media_source_subscription_add_confirm))
}
},
dismissButton = {
TextButton(dismiss) {
- Text("取消")
+ Text(stringResource(Lang.settings_danmaku_cancel))
}
},
)
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/network/ConfigureProxyGroup.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/network/ConfigureProxyGroup.kt
index 896a8a7f03..b182e342c6 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/network/ConfigureProxyGroup.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/settings/tabs/network/ConfigureProxyGroup.kt
@@ -53,14 +53,32 @@ import me.him188.ani.app.ui.foundation.LocalPlatform
import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.text.ProvideContentColor
import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_media_source_edit
import me.him188.ani.app.ui.lang.settings_network_proxy_address
import me.him188.ani.app.ui.lang.settings_network_proxy_address_example
+import me.him188.ani.app.ui.lang.settings_network_proxy_current_disabled
+import me.him188.ani.app.ui.lang.settings_network_proxy_current_using
+import me.him188.ani.app.ui.lang.settings_network_proxy_custom
import me.him188.ani.app.ui.lang.settings_network_proxy_detecting
import me.him188.ani.app.ui.lang.settings_network_proxy_detection_result
+import me.him188.ani.app.ui.lang.settings_network_proxy_disabled
import me.him188.ani.app.ui.lang.settings_network_proxy_none
import me.him188.ani.app.ui.lang.settings_network_proxy_not_detected
import me.him188.ani.app.ui.lang.settings_network_proxy_optional
+import me.him188.ani.app.ui.lang.settings_network_proxy_overall_detecting
+import me.him188.ani.app.ui.lang.settings_network_proxy_overall_failed_not_proxied
+import me.him188.ani.app.ui.lang.settings_network_proxy_overall_failed_proxied
+import me.him188.ani.app.ui.lang.settings_network_proxy_overall_success
import me.him188.ani.app.ui.lang.settings_network_proxy_password
+import me.him188.ani.app.ui.lang.settings_network_proxy_retest
+import me.him188.ani.app.ui.lang.settings_network_proxy_save_and_test
+import me.him188.ani.app.ui.lang.settings_network_proxy_service_collection
+import me.him188.ani.app.ui.lang.settings_network_proxy_service_comment
+import me.him188.ani.app.ui.lang.settings_network_proxy_service_danmaku
+import me.him188.ani.app.ui.lang.settings_network_proxy_system
+import me.him188.ani.app.ui.lang.settings_network_proxy_test_failed
+import me.him188.ani.app.ui.lang.settings_network_proxy_test_success
+import me.him188.ani.app.ui.lang.settings_network_proxy_title
import me.him188.ani.app.ui.lang.settings_network_proxy_username
import me.him188.ani.app.ui.settings.framework.SettingsState
import me.him188.ani.app.ui.settings.framework.components.SettingsScope
@@ -137,11 +155,11 @@ fun SettingsScope.ConfigureProxyGroup(
@Composable
private fun renderOverallTestText(state: ProxyOverallTestState): String {
return when (state) {
- ProxyOverallTestState.INIT -> "正在检测连接,请稍后"
- ProxyOverallTestState.RUNNING -> "正在检测连接,请稍后"
- ProxyOverallTestState.FAILED_NOT_PROXIED -> "部分服务连接失败,请考虑启用代理"
- ProxyOverallTestState.FAILED_PROXIED -> "部分服务连接失败,请更换代理模式或代理地址"
- ProxyOverallTestState.SUCCESS -> "所有服务连接正常"
+ ProxyOverallTestState.INIT -> stringResource(Lang.settings_network_proxy_overall_detecting)
+ ProxyOverallTestState.RUNNING -> stringResource(Lang.settings_network_proxy_overall_detecting)
+ ProxyOverallTestState.FAILED_NOT_PROXIED -> stringResource(Lang.settings_network_proxy_overall_failed_not_proxied)
+ ProxyOverallTestState.FAILED_PROXIED -> stringResource(Lang.settings_network_proxy_overall_failed_proxied)
+ ProxyOverallTestState.SUCCESS -> stringResource(Lang.settings_network_proxy_overall_success)
}
}
@@ -163,7 +181,7 @@ private fun SettingsScope.ProxyTestStatusGroup(
actions = if (state.hasError) {
{
TextButton(onRequestReTest) {
- Text("重新测试")
+ Text(stringResource(Lang.settings_network_proxy_retest))
}
}
} else null,
@@ -185,9 +203,9 @@ private fun renderTestCaseName(case: ProxyTestCase): String {
@Composable
private fun renderTestCaseDescription(case: ProxyTestCase): String {
return when (case.name) {
- ProxyTestCaseEnums.ANI -> "弹幕服务"
- ProxyTestCaseEnums.BANGUMI -> "收藏数据服务"
- ProxyTestCaseEnums.BANGUMI_NEXT -> "评论服务"
+ ProxyTestCaseEnums.ANI -> stringResource(Lang.settings_network_proxy_service_danmaku)
+ ProxyTestCaseEnums.BANGUMI -> stringResource(Lang.settings_network_proxy_service_collection)
+ ProxyTestCaseEnums.BANGUMI_NEXT -> stringResource(Lang.settings_network_proxy_service_comment)
}
}
@@ -223,12 +241,12 @@ private fun ProxyTestStatusIcon(
ProxyTestCaseState.SUCCESS ->
ProvideContentColor(MaterialTheme.colorScheme.onSurfaceVariant) {
- Icon(Icons.Default.Check, "使用 ${state.name} 连接成功")
+ Icon(Icons.Default.Check, stringResource(Lang.settings_network_proxy_test_success))
}
ProxyTestCaseState.FAILED ->
ProvideContentColor(MaterialTheme.colorScheme.error) {
- Icon(Icons.Default.Close, "使用 ${state.name} 连接失败")
+ Icon(Icons.Default.Close, stringResource(Lang.settings_network_proxy_test_failed))
}
}
}
@@ -237,9 +255,9 @@ private fun ProxyTestStatusIcon(
@Composable
private fun renderProxyConfigModeName(mode: ProxyUIMode): String {
return when (mode) {
- ProxyUIMode.DISABLED -> "不使用代理"
- ProxyUIMode.SYSTEM -> "系统代理"
- ProxyUIMode.CUSTOM -> "自定义代理"
+ ProxyUIMode.DISABLED -> stringResource(Lang.settings_network_proxy_disabled)
+ ProxyUIMode.SYSTEM -> stringResource(Lang.settings_network_proxy_system)
+ ProxyUIMode.CUSTOM -> stringResource(Lang.settings_network_proxy_custom)
}
}
@@ -254,7 +272,12 @@ private fun SettingsScope.CurrentProxyTextModePresentation(
title = {
Text(
if (state.config.mode == ProxyUIMode.DISABLED)
- "未启用代理" else "正在使用${renderProxyConfigModeName(state.config.mode)}",
+ stringResource(Lang.settings_network_proxy_current_disabled)
+ else
+ stringResource(
+ Lang.settings_network_proxy_current_using,
+ renderProxyConfigModeName(state.config.mode),
+ ),
)
},
description = when (state.config.mode) {
@@ -276,7 +299,7 @@ private fun SettingsScope.CurrentProxyTextModePresentation(
) {
Icon(Icons.Rounded.Edit, null, Modifier.size(ButtonDefaults.IconSize))
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text("修改", softWrap = false)
+ Text(stringResource(Lang.settings_media_source_edit), softWrap = false)
}
},
)
@@ -303,10 +326,10 @@ private fun SettingsScope.ProxyConfigGroup(
}
Group(
- title = { Text("代理设置") },
+ title = { Text(stringResource(Lang.settings_network_proxy_title)) },
actions = {
TextButton({ onUpdate(currentConfig.value) }) {
- Text("保存并测试")
+ Text(stringResource(Lang.settings_network_proxy_save_and_test))
}
},
useThinHeader = true,
diff --git a/app/shared/ui-settings/src/commonMain/kotlin/ui/update/FailedToInstallDialog.kt b/app/shared/ui-settings/src/commonMain/kotlin/ui/update/FailedToInstallDialog.kt
index 87f1be8345..d78d20893e 100644
--- a/app/shared/ui-settings/src/commonMain/kotlin/ui/update/FailedToInstallDialog.kt
+++ b/app/shared/ui-settings/src/commonMain/kotlin/ui/update/FailedToInstallDialog.kt
@@ -19,7 +19,15 @@ import kotlinx.coroutines.launch
import me.him188.ani.app.platform.LocalContext
import me.him188.ani.app.tools.update.UpdateInstaller
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.settings_update_manual_install_cancel
+import me.him188.ani.app.ui.lang.settings_update_manual_install_open_failed
+import me.him188.ani.app.ui.lang.settings_update_manual_install_package_not_found
+import me.him188.ani.app.ui.lang.settings_update_manual_install_title
+import me.him188.ani.app.ui.lang.settings_update_manual_install_view_package
import me.him188.ani.utils.io.absolutePath
+import org.jetbrains.compose.resources.getString
+import org.jetbrains.compose.resources.stringResource
import org.koin.mp.KoinPlatform
@Composable
@@ -39,23 +47,28 @@ fun FailedToInstallDialog(
scope.launch {
val file = (state as? AppUpdateState.Downloaded)?.file
if (file == null) {
- toaster.toast("未找到安装包")
+ toaster.toast(getString(Lang.settings_update_manual_install_package_not_found))
return@launch
}
val success =
KoinPlatform.getKoin().get().openForManualInstallation(file, context)
if (!success) {
- toaster.toast("打开文件失败,请手动安装 ${file.absolutePath}")
+ toaster.toast(
+ getString(
+ Lang.settings_update_manual_install_open_failed,
+ file.absolutePath,
+ ),
+ )
}
}
},
- ) { Text("查看安装包") }
+ ) { Text(stringResource(Lang.settings_update_manual_install_view_package)) }
},
dismissButton = {
- TextButton(onDismissRequest) { Text("取消更新") }
+ TextButton(onDismissRequest) { Text(stringResource(Lang.settings_update_manual_install_cancel)) }
},
- title = { Text("自动安装失败,请手动安装") },
+ title = { Text(stringResource(Lang.settings_update_manual_install_title)) },
text = { Text(message) },
)
}
diff --git a/app/shared/ui-settings/src/iosMain/kotlin/ui/settings/tabs/log/LogTab.ios.kt b/app/shared/ui-settings/src/iosMain/kotlin/ui/settings/tabs/log/LogTab.ios.kt
index 47d17a9143..15ea453492 100644
--- a/app/shared/ui-settings/src/iosMain/kotlin/ui/settings/tabs/log/LogTab.ios.kt
+++ b/app/shared/ui-settings/src/iosMain/kotlin/ui/settings/tabs/log/LogTab.ios.kt
@@ -25,6 +25,7 @@ import kotlinx.io.files.Path
import kotlinx.io.files.SystemTemporaryDirectory
import me.him188.ani.app.ui.foundation.setClipEntryText
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.utils.io.absolutePath
import me.him188.ani.utils.io.copyTo
import me.him188.ani.utils.io.inSystem
@@ -32,6 +33,7 @@ import me.him188.ani.utils.io.readText
import me.him188.ani.utils.logging.DefaultLoggerFactory
import me.him188.ani.utils.logging.IosLoggingConfigurator
import me.him188.ani.utils.logging.writer.DailyRollingFileLogWriter
+import org.jetbrains.compose.resources.stringResource
import platform.Foundation.NSURL
import platform.UIKit.UIActivityViewController
import platform.UIKit.UIPopoverArrowDirectionAny
@@ -44,15 +46,18 @@ internal actual fun ColumnScope.PlatformLoggingItems(listItemColors: ListItemCol
val uiViewController = LocalUIViewController.current
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
+ val shareTodayLogFileText = stringResource(Lang.settings_log_share_today_log_file)
+ val copyTodayLogContentText = stringResource(Lang.settings_log_copy_today_log_content)
+ val logFileNotFoundText = stringResource(Lang.settings_log_file_not_found)
ListItem(
headlineContent = {
- Text("分享当日日志文件")
+ Text(shareTodayLogFileText)
},
Modifier.clickable {
val file = getTodayLogFile()
if (file == null) {
- toaster.toast("未找到文件")
+ toaster.toast(logFileNotFoundText)
} else {
shareFile(file.inSystem.absolutePath, uiViewController)
}
@@ -62,12 +67,12 @@ internal actual fun ColumnScope.PlatformLoggingItems(listItemColors: ListItemCol
ListItem(
headlineContent = {
- Text("复制当日日志内容 (很大)")
+ Text(copyTodayLogContentText)
},
Modifier.clickable {
val file = getTodayLogFile()
if (file == null) {
- toaster.toast("未找到文件")
+ toaster.toast(logFileNotFoundText)
} else {
scope.launch {
clipboard.setClipEntryText(file.inSystem.readText())
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/CollectionPage.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/CollectionPage.kt
index 6d32eca6f0..99163669a9 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/CollectionPage.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/CollectionPage.kt
@@ -108,6 +108,7 @@ import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.layout.isHeightAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.paneHorizontalPadding
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.session.SelfAvatar
import me.him188.ani.app.ui.foundation.theme.AniThemeDefaults
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
@@ -131,6 +132,7 @@ import me.him188.ani.utils.platform.hasScrollingBug
import me.him188.ani.utils.platform.isDesktop
import me.him188.ani.utils.platform.isMobile
import kotlin.time.Clock
+import org.jetbrains.compose.resources.*
// 有顺序, https://github.com/Him188/ani/issues/73
@@ -240,7 +242,7 @@ fun CollectionPage(
|| currentWindowAdaptiveInfo1().windowSizeClass.isWidthAtLeastMedium
) {
IconButton(onClick = onClickSettings) {
- Icon(Icons.Rounded.Settings, "设置")
+ Icon(Icons.Rounded.Settings, stringResource(Lang.settings))
}
}
},
@@ -260,7 +262,7 @@ fun CollectionPage(
IconButton({ hideBangumiSync = false }) {
Icon(
imageVector = Icons.Rounded.Sync,
- contentDescription = "正在同步",
+ contentDescription = stringResource(Lang.subject_collection_syncing),
modifier = Modifier.rotate(angle),
)
}
@@ -395,7 +397,7 @@ private fun CollectionPageLayout(
topBar = {
Column(modifier = Modifier.fillMaxWidth()) {
AniTopAppBar(
- title = { AniTopAppBarDefaults.Title("追番") },
+ title = { AniTopAppBarDefaults.Title(stringResource(Lang.subject_collection_page_title)) },
modifier = Modifier,
actions = {
actions()
@@ -616,7 +618,11 @@ private fun SubjectCollectionItem(
},
enabled = !editableSubjectCollectionTypePresentation.isSetSelfCollectionTypeWorking,
) {
- Text("移至\"看过\"", Modifier.requiredWidth(IntrinsicSize.Max), softWrap = false)
+ Text(
+ stringResource(Lang.subject_collection_move_to_watched),
+ Modifier.requiredWidth(IntrinsicSize.Max),
+ softWrap = false,
+ )
}
} else {
SubjectProgressButton(
@@ -635,15 +641,16 @@ private fun SubjectCollectionItem(
)
}
+@Composable
@Stable
private fun UnifiedCollectionType.displayText(): String {
return when (this) {
- UnifiedCollectionType.WISH -> "想看"
- UnifiedCollectionType.DOING -> "在看"
- UnifiedCollectionType.DONE -> "看过"
- UnifiedCollectionType.ON_HOLD -> "搁置"
- UnifiedCollectionType.DROPPED -> "抛弃"
- UnifiedCollectionType.NOT_COLLECTED -> "未收藏"
+ UnifiedCollectionType.WISH -> stringResource(Lang.subject_collection_wish)
+ UnifiedCollectionType.DOING -> stringResource(Lang.subject_collection_doing)
+ UnifiedCollectionType.DONE -> stringResource(Lang.subject_collection_done)
+ UnifiedCollectionType.ON_HOLD -> stringResource(Lang.subject_collection_on_hold)
+ UnifiedCollectionType.DROPPED -> stringResource(Lang.subject_collection_dropped)
+ UnifiedCollectionType.NOT_COLLECTED -> stringResource(Lang.subject_collection_uncollected)
}
}
@@ -655,17 +662,17 @@ private fun GuestTips(
modifier: Modifier = Modifier,
) {
Column(modifier) {
- Text("游客模式下请搜索后观看,或登录后使用收藏功能")
+ Text(stringResource(Lang.subject_collection_guest_mode_tip))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedButton(onClickLogin, Modifier.weight(1f)) {
Icon(Icons.Rounded.HowToReg, null)
- Text("登录", Modifier.padding(start = 8.dp))
+ Text(stringResource(Lang.login_sign_in), Modifier.padding(start = 8.dp))
}
Button(onClickSearch, Modifier.weight(1f)) {
Icon(Icons.Rounded.Search, null)
- Text("搜索", Modifier.padding(start = 8.dp))
+ Text(stringResource(Lang.exploration_search), Modifier.padding(start = 8.dp))
}
}
}
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/SubjectCollectionTypeSuggestions.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/SubjectCollectionTypeSuggestions.kt
index bc889fb5f2..89cf714b69 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/SubjectCollectionTypeSuggestions.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/SubjectCollectionTypeSuggestions.kt
@@ -21,11 +21,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import kotlinx.coroutines.launch
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.foundation.widgets.showLoadError
import me.him188.ani.app.ui.subject.collection.components.EditableSubjectCollectionTypeState
import me.him188.ani.app.ui.subject.collection.components.SubjectCollectionActions
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
+import org.jetbrains.compose.resources.*
object SubjectCollectionTypeSuggestions {
@Composable
@@ -62,7 +64,7 @@ object SubjectCollectionTypeSuggestions {
targetType = UnifiedCollectionType.DOING,
state = state,
icon = { Icon(Icons.Rounded.Star, null) },
- label = { Text("追番") },
+ label = { Text(stringResource(Lang.subject_collection_collect)) },
colors = SuggestionChipDefaults.suggestionChipColors(
labelColor = MaterialTheme.colorScheme.primary,
iconContentColor = MaterialTheme.colorScheme.primary,
@@ -78,7 +80,7 @@ object SubjectCollectionTypeSuggestions {
targetType = UnifiedCollectionType.DOING,
state = state,
icon = SubjectCollectionActions.Doing.icon,
- label = { Text("在看") },
+ label = { Text(stringResource(Lang.subject_collection_doing)) },
modifier = modifier,
)
@@ -90,7 +92,7 @@ object SubjectCollectionTypeSuggestions {
targetType = UnifiedCollectionType.DROPPED,
state = state,
icon = SubjectCollectionActions.Dropped.icon,
- label = { Text("抛弃") },
+ label = { Text(stringResource(Lang.subject_collection_dropped)) },
modifier = modifier,
)
}
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/SubjectCollectionsColumn.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/SubjectCollectionsColumn.kt
index 78f4b9229d..d4b9448963 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/SubjectCollectionsColumn.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/SubjectCollectionsColumn.kt
@@ -70,6 +70,7 @@ import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
import me.him188.ani.app.ui.foundation.ifThen
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.layout.isWidthCompact
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.stateOf
import me.him188.ani.app.ui.search.LoadErrorCard
import me.him188.ani.app.ui.search.isLoadingNextPage
@@ -82,6 +83,7 @@ import me.him188.ani.app.ui.subject.collection.progress.SubjectProgressButton
import me.him188.ani.app.ui.subject.collection.progress.rememberTestSubjectProgressState
import me.him188.ani.app.ui.subject.details.components.COVER_WIDTH_TO_HEIGHT_RATIO
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.*
/**
* 用户的收藏列表.
@@ -273,7 +275,7 @@ private fun SubjectCollectionItemContent(
verticalAlignment = Alignment.CenterVertically,
) {
TextButton(onShowEpisodeList) {
- Text("选集")
+ Text(stringResource(Lang.video_player_select_episode))
}
Box(Modifier.width(IntrinsicSize.Min)) { playButton() }
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/EditCollectionTypeDropDown.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/EditCollectionTypeDropDown.kt
index b1826d6601..cf3c318e1c 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/EditCollectionTypeDropDown.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/EditCollectionTypeDropDown.kt
@@ -30,7 +30,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.foundation.widgets.showLoadError
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
+import org.jetbrains.compose.resources.*
@Composable
@@ -109,8 +111,8 @@ fun EditCollectionTypeDropDown(
if (showConfirmDeleteDialog) {
AlertDialog(
onDismissRequest = { showConfirmDeleteDialog = false },
- title = { Text("取消追番") },
- text = { Text("这将会清除你的观看进度和评价。此操作无法撤销。确定要取消追番吗?") },
+ title = { Text(stringResource(Lang.subject_collection_delete_confirm_title)) },
+ text = { Text(stringResource(Lang.subject_collection_delete_confirm_message)) },
icon = { SubjectCollectionActions.DeleteCollection.icon() },
confirmButton = {
TextButton(
@@ -120,14 +122,17 @@ fun EditCollectionTypeDropDown(
showConfirmDeleteDialog = false
},
) {
- Text("删除", color = MaterialTheme.colorScheme.error)
+ Text(
+ stringResource(Lang.subject_collection_delete_action),
+ color = MaterialTheme.colorScheme.error,
+ )
}
},
dismissButton = {
TextButton(
onClick = { showConfirmDeleteDialog = false },
) {
- Text("取消")
+ Text(stringResource(Lang.subject_collection_cancel))
}
},
)
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/EditableSubjectCollectionTypeButton.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/EditableSubjectCollectionTypeButton.kt
index efd570825b..bb4a0dd219 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/EditableSubjectCollectionTypeButton.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/EditableSubjectCollectionTypeButton.kt
@@ -39,10 +39,12 @@ import kotlinx.coroutines.launch
import me.him188.ani.app.domain.foundation.LoadError
import me.him188.ani.app.tools.MonoTasker
import me.him188.ani.app.ui.external.placeholder.placeholder
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.foundation.widgets.showLoadError
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.*
import kotlin.coroutines.cancellation.CancellationException
@Stable
@@ -204,15 +206,15 @@ private fun SetAllEpisodeDoneDialog(
AlertDialog(
onDismissRequest = onDismissRequest,
icon = { Icon(Icons.Rounded.TaskAlt, null) },
- text = { Text("要同时设置所有剧集为看过吗?") },
+ text = { Text(stringResource(Lang.subject_collection_set_all_episodes_watched)) },
confirmButton = {
- TextButton(onConfirm) { Text("设置") }
+ TextButton(onConfirm) { Text(stringResource(Lang.subject_collection_set)) }
if (isWorking) {
CircularProgressIndicator(Modifier.padding(start = 8.dp).size(24.dp))
}
},
- dismissButton = { TextButton(onDismissRequest) { Text("忽略") } },
+ dismissButton = { TextButton(onDismissRequest) { Text(stringResource(Lang.subject_collection_ignore)) } },
modifier = modifier,
)
}
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/SubjectCollectionAction.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/SubjectCollectionAction.kt
index 4fdc4dba61..d3200b81b9 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/SubjectCollectionAction.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/SubjectCollectionAction.kt
@@ -23,7 +23,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
+import org.jetbrains.compose.resources.*
/**
* 收藏类型的展示图标和标题. 用于给各种需要展示收藏类型的地方提供一致的展示方式.
@@ -39,49 +41,49 @@ class SubjectCollectionAction(
object SubjectCollectionActions {
@Stable
val Wish = SubjectCollectionAction(
- { Text("想看") },
+ { Text(stringResource(Lang.subject_collection_wish)) },
{ Icon(Icons.AutoMirrored.Rounded.EventNote, null) },
UnifiedCollectionType.WISH,
)
@Stable
val Doing = SubjectCollectionAction(
- { Text("在看") },
+ { Text(stringResource(Lang.subject_collection_doing)) },
{ Icon(Icons.Rounded.PlayCircleOutline, null) },
UnifiedCollectionType.DOING,
)
@Stable
val Done = SubjectCollectionAction(
- { Text("看过") },
+ { Text(stringResource(Lang.subject_collection_done)) },
{ Icon(Icons.Rounded.TaskAlt, null) },
UnifiedCollectionType.DONE,
)
@Stable
val OnHold = SubjectCollectionAction(
- { Text("搁置") },
+ { Text(stringResource(Lang.subject_collection_on_hold)) },
{ Icon(Icons.Rounded.AccessTime, null) },
UnifiedCollectionType.ON_HOLD,
)
@Stable
val Dropped = SubjectCollectionAction(
- { Text("抛弃") },
+ { Text(stringResource(Lang.subject_collection_dropped)) },
{ Icon(Icons.Rounded.Block, null) },
UnifiedCollectionType.DROPPED,
)
@Stable
val DeleteCollection = SubjectCollectionAction(
- { Text("取消追番", color = MaterialTheme.colorScheme.error) },
+ { Text(stringResource(Lang.subject_collection_delete), color = MaterialTheme.colorScheme.error) },
{ Icon(Icons.Rounded.DeleteOutline, null) },
type = UnifiedCollectionType.NOT_COLLECTED,
)
@Stable
val Collect = SubjectCollectionAction(
- { Text("追番") },
+ { Text(stringResource(Lang.subject_collection_collect)) },
{ Icon(Icons.Rounded.Star, null) },
type = UnifiedCollectionType.NOT_COLLECTED,
)
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/SubjectCollectionTypeButton.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/SubjectCollectionTypeButton.kt
index 57a0f3346e..88044fb186 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/SubjectCollectionTypeButton.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/components/SubjectCollectionTypeButton.kt
@@ -32,7 +32,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.datasources.api.topic.UnifiedCollectionType
+import org.jetbrains.compose.resources.*
object SubjectCollectionTypeButtonDefaults {
@Composable
@@ -90,7 +92,7 @@ fun SubjectCollectionTypeButton(
Text(renderCollectionTypeAsCurrent(type))
}
} else {
- Text("载入") // 随便什么都行, 占空间
+ Text("Loading") // Placeholder to preserve layout
}
}
} else {
@@ -104,7 +106,7 @@ fun SubjectCollectionTypeButton(
action.title()
}
} else {
- Text("载入") // 随便什么都行, 占空间
+ Text("Loading") // Placeholder to preserve layout
}
}
@@ -121,15 +123,16 @@ fun SubjectCollectionTypeButton(
}
}
+@Composable
@Stable
private fun renderCollectionTypeAsCurrent(type: UnifiedCollectionType): String {
return when (type) {
- UnifiedCollectionType.WISH -> "已想看"
- UnifiedCollectionType.DOING -> "已在看"
- UnifiedCollectionType.DONE -> "已看过"
- UnifiedCollectionType.ON_HOLD -> "已搁置"
- UnifiedCollectionType.DROPPED -> "已抛弃"
- UnifiedCollectionType.NOT_COLLECTED -> "未追番"
+ UnifiedCollectionType.WISH -> stringResource(Lang.subject_collection_current_wish)
+ UnifiedCollectionType.DOING -> stringResource(Lang.subject_collection_current_doing)
+ UnifiedCollectionType.DONE -> stringResource(Lang.subject_collection_current_done)
+ UnifiedCollectionType.ON_HOLD -> stringResource(Lang.subject_collection_current_on_hold)
+ UnifiedCollectionType.DROPPED -> stringResource(Lang.subject_collection_current_dropped)
+ UnifiedCollectionType.NOT_COLLECTED -> stringResource(Lang.subject_collection_not_collected)
}
}
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/progress/SubjectProgressButton.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/progress/SubjectProgressButton.kt
index ca3d524146..771f8a2a36 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/progress/SubjectProgressButton.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/collection/progress/SubjectProgressButton.kt
@@ -18,6 +18,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import me.him188.ani.app.ui.subject.SubjectProgressState
+import me.him188.ani.app.ui.subject.rememberSubjectStatusStrings
/**
@@ -32,14 +33,15 @@ fun SubjectProgressButton(
modifier: Modifier = Modifier,
) {
val requiredWidth = Modifier.requiredWidth(IntrinsicSize.Max)
+ val strings = rememberSubjectStatusStrings()
Crossfade(state.buttonIsPrimary) { isPrimary ->
if (isPrimary) {
Button(onClick = onPlay, modifier) {
- Text(state.buttonText, requiredWidth, softWrap = false)
+ Text(state.buttonText(strings), requiredWidth, softWrap = false)
}
} else {
FilledTonalButton(onClick = onPlay, modifier) {
- Text(state.buttonText, requiredWidth, softWrap = false)
+ Text(state.buttonText(strings), requiredWidth, softWrap = false)
}
}
}
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/SubjectDetailsPage.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/SubjectDetailsPage.kt
index f1bd981b33..9ca9d1bf13 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/SubjectDetailsPage.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/SubjectDetailsPage.kt
@@ -121,6 +121,15 @@ import me.him188.ani.app.ui.foundation.toComposeImageBitmap
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.foundation.widgets.showLoadError
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.foundation_richtext_external_app_link_warning_prefix
+import me.him188.ani.app.ui.lang.foundation_richtext_open_failed_prefix
+import me.him188.ani.app.ui.lang.subject_details_coming_soon
+import me.him188.ani.app.ui.lang.subject_details_login_to_collect
+import me.him188.ani.app.ui.lang.subject_details_tab_comments
+import me.him188.ani.app.ui.lang.subject_details_tab_details
+import me.him188.ani.app.ui.lang.subject_details_tab_discussions
+import me.him188.ani.app.ui.lang.subject_details_write_review
import me.him188.ani.app.ui.rating.EditableRating
import me.him188.ani.app.ui.rating.EditableRatingState
import me.him188.ani.app.ui.richtext.RichTextDefaults
@@ -146,6 +155,7 @@ import me.him188.ani.datasources.api.PackedDate
import me.him188.ani.datasources.api.topic.toggleCollected
import me.him188.ani.utils.platform.annotations.TestOnly
import me.him188.ani.utils.platform.isMobile
+import org.jetbrains.compose.resources.stringResource
// region screen
@@ -271,6 +281,8 @@ private fun SubjectDetailsPage(
val toaster = LocalToaster.current
val browserNavigator = LocalUriHandler.current
val navigator = LocalNavigator.current
+ val externalAppLinkWarningPrefix = stringResource(Lang.foundation_richtext_external_app_link_warning_prefix)
+ val openLinkFailedPrefix = stringResource(Lang.foundation_richtext_open_failed_prefix)
var showSelectEpisode by rememberSaveable { mutableStateOf(false) }
val connectedScrollState = rememberConnectedScrollState()
@@ -316,7 +328,7 @@ private fun SubjectDetailsPage(
collectionActions = {
if (selfInfo.isSessionValid == false) {
OutlinedButton(onClickLogin) {
- Text("登录后可收藏")
+ Text(stringResource(Lang.subject_details_login_to_collect))
}
} else {
EditableSubjectCollectionTypeButton(state.editableSubjectCollectionTypeState)
@@ -372,7 +384,13 @@ private fun SubjectDetailsPage(
SubjectDetailsDefaults.SubjectCommentColumn(
state = state.subjectCommentState,
onClickUrl = {
- RichTextDefaults.checkSanityAndOpen(it, browserNavigator, toaster)
+ RichTextDefaults.checkSanityAndOpen(
+ it,
+ browserNavigator,
+ toaster,
+ externalAppLinkWarningPrefix,
+ openLinkFailedPrefix,
+ )
},
onClickImage = { imageViewer.viewImage(it) },
connectedScrollState,
@@ -389,7 +407,7 @@ private fun SubjectDetailsPage(
) {
item {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
- Text("即将上线, 敬请期待", Modifier.padding(16.dp))
+ Text(stringResource(Lang.subject_details_coming_soon), Modifier.padding(16.dp))
}
}
}
@@ -435,7 +453,7 @@ private fun PlaceholderSubjectDetailsPage(
OutlinedButton(
onClick = {},
modifier = Modifier.placeholder(true),
- ) { Text("登录后可收藏") }
+ ) { Text(stringResource(Lang.subject_details_login_to_collect)) }
},
rating = {
val scope = rememberCoroutineScope()
@@ -677,7 +695,7 @@ private fun SubjectDetailsContentPager(
SubjectDetailsTab.DETAILS -> {}
SubjectDetailsTab.COMMENTS -> {
ExtendedFloatingActionButton(
- text = { Text("写评价") },
+ text = { Text(stringResource(Lang.subject_details_write_review)) },
icon = {
Icon(Icons.Rounded.AddComment, null)
},
@@ -928,11 +946,12 @@ sealed interface SubjectDetailsUIState {
}
@Stable
+@Composable
private fun renderSubjectDetailsTab(tab: SubjectDetailsTab): String {
return when (tab) {
- SubjectDetailsTab.DETAILS -> "详情"
- SubjectDetailsTab.COMMENTS -> "评价"
- SubjectDetailsTab.DISCUSSIONS -> "讨论"
+ SubjectDetailsTab.DETAILS -> stringResource(Lang.subject_details_tab_details)
+ SubjectDetailsTab.COMMENTS -> stringResource(Lang.subject_details_tab_comments)
+ SubjectDetailsTab.DISCUSSIONS -> stringResource(Lang.subject_details_tab_discussions)
}
}
@@ -994,4 +1013,4 @@ private fun PreviewSubjectDetailsScreen(
modifier = modifier,
navigationIcon = { BackNavigationIconButton({}) },
)
-}
\ No newline at end of file
+}
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/DetailsTab.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/DetailsTab.kt
index f6b6821db7..9cafb41fdf 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/DetailsTab.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/DetailsTab.kt
@@ -82,6 +82,15 @@ import me.him188.ani.app.ui.foundation.layout.desktopTitleBar
import me.him188.ani.app.ui.foundation.layout.desktopTitleBarPadding
import me.him188.ani.app.ui.foundation.layout.paneHorizontalPadding
import me.him188.ani.app.ui.foundation.layout.plus
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_details_characters
+import me.him188.ani.app.ui.lang.subject_details_characters_with_count
+import me.him188.ani.app.ui.lang.subject_details_related_subjects
+import me.him188.ani.app.ui.lang.subject_details_show_less
+import me.him188.ani.app.ui.lang.subject_details_show_more
+import me.him188.ani.app.ui.lang.subject_details_staff
+import me.him188.ani.app.ui.lang.subject_details_staff_with_count
+import me.him188.ani.app.ui.lang.subject_details_view_all
import me.him188.ani.app.ui.search.rememberTestLazyPagingItems
import me.him188.ani.app.ui.subject.AiringLabel
import me.him188.ani.app.ui.subject.AiringLabelState
@@ -91,6 +100,7 @@ import me.him188.ani.app.ui.subject.details.TestSubjectStaffInfo
import me.him188.ani.app.ui.subject.renderSubjectSeason
import me.him188.ani.datasources.api.PackedDate
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
object SubjectDetailsDefaults {
@@ -148,6 +158,15 @@ fun SubjectDetailsDefaults.DetailsTab(
horizontalPadding: Dp = currentWindowAdaptiveInfo1().windowSizeClass.paneHorizontalPadding,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
+ val charactersText = stringResource(Lang.subject_details_characters)
+ val charactersWithCountText = totalCharactersCount?.let {
+ stringResource(Lang.subject_details_characters_with_count, it)
+ }
+ val staffText = stringResource(Lang.subject_details_staff)
+ val staffWithCountText = totalStaffCount?.let {
+ stringResource(Lang.subject_details_staff_with_count, it)
+ }
+ val relatedSubjectsText = stringResource(Lang.subject_details_related_subjects)
LazyColumn(
modifier,
state = state,
@@ -181,7 +200,7 @@ fun SubjectDetailsDefaults.DetailsTab(
item("characters title") {
Text(
- "角色",
+ charactersText,
Modifier.padding(horizontal = horizontalPadding),
style = MaterialTheme.typography.titleMedium,
)
@@ -193,7 +212,7 @@ fun SubjectDetailsDefaults.DetailsTab(
exposedCharacters,
sheetTitle = {
Text(
- if (totalCharactersCount == null) "角色" else "角色 $totalCharactersCount",
+ charactersWithCountText ?: charactersText,
)
},
modifier = Modifier.padding(horizontal = horizontalPadding),
@@ -203,7 +222,7 @@ fun SubjectDetailsDefaults.DetailsTab(
item("staff title") {
Text(
- "制作人员",
+ staffText,
Modifier.padding(horizontal = horizontalPadding),
style = MaterialTheme.typography.titleMedium,
)
@@ -215,7 +234,7 @@ fun SubjectDetailsDefaults.DetailsTab(
exposedStaff,
sheetTitle = {
Text(
- if (totalStaffCount == null) "制作人员" else "制作人员 $totalStaffCount",
+ staffWithCountText ?: staffText,
)
},
modifier = Modifier.padding(horizontal = horizontalPadding),
@@ -226,7 +245,7 @@ fun SubjectDetailsDefaults.DetailsTab(
if (relatedSubjects.itemCount != 0) {
item("related subjects title") {
Text(
- "关联条目",
+ relatedSubjectsText,
Modifier.padding(horizontal = horizontalPadding),
style = MaterialTheme.typography.titleMedium,
)
@@ -267,6 +286,8 @@ private fun TagsList(
onClickTag: (Tag) -> Unit,
modifier: Modifier = Modifier,
) {
+ val showLessText = stringResource(Lang.subject_details_show_less)
+ val showMoreText = stringResource(Lang.subject_details_show_more)
Column(modifier) {
val allTags by remember(info) {
derivedStateOf { info.tags }
@@ -328,14 +349,14 @@ private fun TagsList(
{ isExpanded = !isExpanded },
Modifier.height(40.dp),
) {
- Text("显示更少")
+ Text(showLessText)
}
} else {
TextButton(
{ isExpanded = !isExpanded },
Modifier.height(40.dp),
) {
- Text("显示更多")
+ Text(showMoreText)
}
}
}
@@ -354,6 +375,7 @@ private fun PersonCardList(
itemSpacing: Dp = 12.dp,
itemContent: @Composable (T) -> Unit,
) {
+ val viewAllText = stringResource(Lang.subject_details_view_all)
Column(modifier) {
var showSheet by rememberSaveable { mutableStateOf(false) }
FlowRow(
@@ -372,7 +394,7 @@ private fun PersonCardList(
{ showSheet = true },
Modifier.padding(top = 8.dp).align(Alignment.End),
) {
- Text("查看全部")
+ Text(viewAllText)
}
if (showSheet) {
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/RelatedSubjectsRow.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/RelatedSubjectsRow.kt
index 5988b0e8d6..b8473583b0 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/RelatedSubjectsRow.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/RelatedSubjectsRow.kt
@@ -49,9 +49,15 @@ import me.him188.ani.app.data.models.subject.SubjectRelation
import me.him188.ani.app.platform.currentAniBuildConfig
import me.him188.ani.app.ui.foundation.AsyncImage
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_details_relation_derived
+import me.him188.ani.app.ui.lang.subject_details_relation_prequel
+import me.him188.ani.app.ui.lang.subject_details_relation_sequel
+import me.him188.ani.app.ui.lang.subject_details_relation_special
import me.him188.ani.app.ui.search.createTestPager
import me.him188.ani.app.ui.subject.details.TestRelatedSubjects
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.stringResource
import kotlin.math.ceil
@Composable
@@ -114,6 +120,10 @@ private fun RelatedSubjectItem(
modifier: Modifier = Modifier,
height: Dp = 120.dp,
) {
+ val prequelText = stringResource(Lang.subject_details_relation_prequel)
+ val sequelText = stringResource(Lang.subject_details_relation_sequel)
+ val derivedText = stringResource(Lang.subject_details_relation_derived)
+ val specialText = stringResource(Lang.subject_details_relation_special)
Card(
onClick,
modifier,
@@ -151,10 +161,10 @@ private fun RelatedSubjectItem(
Box(Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
Text(
when (it) {
- SubjectRelation.PREQUEL -> "前传"
- SubjectRelation.SEQUEL -> "续集"
- SubjectRelation.DERIVED -> "衍生"
- SubjectRelation.SPECIAL -> "番外篇"
+ SubjectRelation.PREQUEL -> prequelText
+ SubjectRelation.SEQUEL -> sequelText
+ SubjectRelation.DERIVED -> derivedText
+ SubjectRelation.SPECIAL -> specialText
},
)
}
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/SubjectDetailsCollectionData.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/SubjectDetailsCollectionData.kt
index 3f9bd577b5..526b55e034 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/SubjectDetailsCollectionData.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/SubjectDetailsCollectionData.kt
@@ -16,10 +16,13 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import me.him188.ani.app.data.models.subject.SubjectCollectionStats
import me.him188.ani.app.ui.foundation.theme.slightlyWeaken
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_details_collection_dropped
+import me.him188.ani.app.ui.lang.subject_details_collection_summary
+import org.jetbrains.compose.resources.stringResource
// 详情页内容 (不包含背景)
@Composable
@@ -31,16 +34,16 @@ fun SubjectDetailsDefaults.CollectionData(
Row(modifier) {
val collection = collectionStats
Text(
- remember(collection) {
- "${collection.collect} 收藏 / ${collection.doing} 在看"
- },
+ stringResource(
+ Lang.subject_details_collection_summary,
+ collection.collect.toString(),
+ collection.doing.toString(),
+ ),
maxLines = 1,
style = MaterialTheme.typography.labelLarge,
)
Text(
- remember(collection) {
- " / ${collection.dropped} 抛弃"
- },
+ stringResource(Lang.subject_details_collection_dropped, collection.dropped.toString()),
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
color = LocalContentColor.current.slightlyWeaken(),
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/episode/list/EpisodeList.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/episode/list/EpisodeList.kt
index 58644c962b..82114d7a0e 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/episode/list/EpisodeList.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/episode/list/EpisodeList.kt
@@ -40,9 +40,11 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import me.him188.ani.app.data.models.preference.EpisodeListProgressTheme
import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.theme.stronglyWeaken
import me.him188.ani.app.ui.foundation.theme.weaken
import me.him188.ani.utils.platform.annotations.TestOnly
+import org.jetbrains.compose.resources.*
@Composable
fun EpisodeListDialog(
@@ -59,7 +61,10 @@ fun EpisodeListDialog(
Box {
Column(Modifier.padding(16.dp)) {
Row {
- Text("选集播放", style = MaterialTheme.typography.titleLarge)
+ Text(
+ stringResource(Lang.subject_episode_select_play),
+ style = MaterialTheme.typography.titleLarge,
+ )
Spacer(Modifier.weight(1f))
}
@@ -101,7 +106,10 @@ fun EpisodeListDialog(
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Outlined.Lightbulb, null)
- Text("长按还可以标记为已看", Modifier.padding(start = 4.dp))
+ Text(
+ stringResource(Lang.subject_episode_long_press_mark_watched),
+ Modifier.padding(start = 4.dp),
+ )
}
Row(Modifier.padding(top = 16.dp).align(Alignment.End)) {
@@ -112,18 +120,18 @@ fun EpisodeListDialog(
it()
},
) {
- Text("条目详情")
+ Text(stringResource(Lang.subject_episode_details))
}
}
TextButton(onDismissRequest, Modifier.padding(start = 8.dp)) {
- Text("关闭")
+ Text(stringResource(Lang.subject_episode_close))
}
}
}
IconButton(onCacheClick, Modifier.align(Alignment.TopEnd).padding(8.dp)) {
- Icon(Icons.Rounded.Download, "缓存")
+ Icon(Icons.Rounded.Download, stringResource(Lang.subject_episode_cache))
}
}
}
diff --git a/app/shared/video-player/src/commonMain/kotlin/ui/VideoAspectControllerState.kt b/app/shared/video-player/src/commonMain/kotlin/ui/VideoAspectControllerState.kt
index 7561b8f458..d0396887ad 100644
--- a/app/shared/video-player/src/commonMain/kotlin/ui/VideoAspectControllerState.kt
+++ b/app/shared/video-player/src/commonMain/kotlin/ui/VideoAspectControllerState.kt
@@ -19,9 +19,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
+import me.him188.ani.app.ui.lang.*
import org.openani.mediamp.InternalForInheritanceMediampApi
import org.openani.mediamp.features.AspectRatioMode
import org.openani.mediamp.features.VideoAspectRatio
+import org.jetbrains.compose.resources.*
@Stable
class VideoAspectRatioControllerState(
@@ -51,9 +53,9 @@ class VideoAspectRatioControllerState(
@Composable
fun renderAspectRatioMode(mode: AspectRatioMode): String {
return when (mode) {
- AspectRatioMode.FIT -> "适应"
- AspectRatioMode.STRETCH -> "填充"
- AspectRatioMode.CROP -> "裁切"
+ AspectRatioMode.FIT -> stringResource(Lang.video_player_aspect_fit)
+ AspectRatioMode.STRETCH -> stringResource(Lang.video_player_aspect_stretch)
+ AspectRatioMode.CROP -> stringResource(Lang.video_player_aspect_crop)
}
}
@@ -63,4 +65,4 @@ object NoOpVideoAspectRatio : VideoAspectRatio {
override fun setMode(mode: AspectRatioMode) {
}
-}
\ No newline at end of file
+}
diff --git a/app/shared/video-player/src/commonMain/kotlin/ui/progress/AudioSwitcher.kt b/app/shared/video-player/src/commonMain/kotlin/ui/progress/AudioSwitcher.kt
index 70e81aa685..71d723dc20 100644
--- a/app/shared/video-player/src/commonMain/kotlin/ui/progress/AudioSwitcher.kt
+++ b/app/shared/video-player/src/commonMain/kotlin/ui/progress/AudioSwitcher.kt
@@ -29,8 +29,10 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import me.him188.ani.app.ui.foundation.AbstractViewModel
import me.him188.ani.app.ui.foundation.dialogs.PlatformPopupProperties
+import me.him188.ani.app.ui.lang.*
import org.openani.mediamp.metadata.AudioTrack
import org.openani.mediamp.metadata.TrackGroup
+import org.jetbrains.compose.resources.*
@Stable
class AudioTrackState(
@@ -99,14 +101,15 @@ fun PlayerControllerDefaults.AudioSwitcher(
optionsProvider = { options },
renderValue = {
if (it == null) {
- Text("自动")
+ Text(stringResource(Lang.video_player_auto))
} else {
Text(it.displayName, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
},
renderValueExposed = {
+ val audioTrackText = stringResource(Lang.video_player_audio_track)
Text(
- remember(it) { it?.displayName ?: "音轨" },
+ remember(it, audioTrackText) { it?.displayName ?: audioTrackText },
Modifier.widthIn(max = 64.dp),
maxLines = 1, overflow = TextOverflow.Ellipsis,
)
diff --git a/app/shared/video-player/src/commonMain/kotlin/ui/progress/PlayerControllerBar.kt b/app/shared/video-player/src/commonMain/kotlin/ui/progress/PlayerControllerBar.kt
index 0d27790ff5..af04c23457 100644
--- a/app/shared/video-player/src/commonMain/kotlin/ui/progress/PlayerControllerBar.kt
+++ b/app/shared/video-player/src/commonMain/kotlin/ui/progress/PlayerControllerBar.kt
@@ -97,6 +97,7 @@ import me.him188.ani.app.domain.media.player.MediaCacheProgressInfo
import me.him188.ani.app.ui.foundation.dialogs.PlatformPopupProperties
import me.him188.ani.app.ui.foundation.effects.onKey
import me.him188.ani.app.ui.foundation.ifThen
+import me.him188.ani.app.ui.lang.*
import me.him188.ani.app.ui.foundation.theme.AniTheme
import me.him188.ani.app.ui.foundation.theme.slightlyWeaken
import me.him188.ani.app.ui.foundation.theme.stronglyWeaken
@@ -106,6 +107,7 @@ import me.him188.ani.app.videoplayer.ui.VideoAspectRatioControllerState
import me.him188.ani.app.videoplayer.ui.renderAspectRatioMode
import me.him188.ani.app.videoplayer.ui.top.needWorkaroundForFocusManager
import kotlin.math.roundToInt
+import org.jetbrains.compose.resources.*
const val TAG_SELECT_EPISODE_ICON_BUTTON = "SelectEpisodeIconButton"
const val TAG_SPEED_SWITCHER_TEXT_BUTTON = "SpeedSwitcherTextButton"
@@ -151,9 +153,9 @@ object PlayerControllerDefaults {
modifier.testTag(TAG_DANMAKU_ICON_BUTTON),
) {
if (danmakuEnabled) {
- Icon(Icons.Rounded.Subtitles, contentDescription = "禁用弹幕")
+ Icon(Icons.Rounded.Subtitles, contentDescription = stringResource(Lang.video_player_disable_danmaku))
} else {
- Icon(Icons.Rounded.SubtitlesOff, contentDescription = "启用弹幕")
+ Icon(Icons.Rounded.SubtitlesOff, contentDescription = stringResource(Lang.video_player_enable_danmaku))
}
}
}
@@ -187,19 +189,31 @@ object PlayerControllerDefaults {
) {
when {
isMute -> {
- Icon(Icons.AutoMirrored.Rounded.VolumeOff, contentDescription = "静音")
+ Icon(
+ Icons.AutoMirrored.Rounded.VolumeOff,
+ contentDescription = stringResource(Lang.video_player_mute),
+ )
}
volume < 0.33f -> {
- Icon(Icons.AutoMirrored.Rounded.VolumeMute, contentDescription = "音量")
+ Icon(
+ Icons.AutoMirrored.Rounded.VolumeMute,
+ contentDescription = stringResource(Lang.video_player_volume),
+ )
}
volume < 0.66f -> {
- Icon(Icons.AutoMirrored.Rounded.VolumeDown, contentDescription = "音量")
+ Icon(
+ Icons.AutoMirrored.Rounded.VolumeDown,
+ contentDescription = stringResource(Lang.video_player_volume),
+ )
}
else -> {
- Icon(Icons.AutoMirrored.Rounded.VolumeUp, contentDescription = "音量")
+ Icon(
+ Icons.AutoMirrored.Rounded.VolumeUp,
+ contentDescription = stringResource(Lang.video_player_volume),
+ )
}
}
}
@@ -274,7 +288,7 @@ object PlayerControllerDefaults {
onClick,
modifier,
) {
- Icon(Icons.Rounded.SkipNext, "下一集", Modifier.size(36.dp))
+ Icon(Icons.Rounded.SkipNext, stringResource(Lang.video_player_next_episode), Modifier.size(36.dp))
}
}
@@ -290,37 +304,41 @@ object PlayerControllerDefaults {
contentColor = LocalContentColor.current,
),
) {
- Text("选集")
+ Text(stringResource(Lang.video_player_select_episode))
}
}
- // TODO: DANMAKU_PLACEHOLDERS i18n
- // See #120
- @Stable
- private val DANMAKU_PLACEHOLDERS = listOf(
- "来发一条弹幕吧~",
- "小心,我要发射弹幕啦!",
- "每一条弹幕背后,都有一个不为人知的秘密",
- "召唤弹幕精灵!",
- "这一刻的感受,只有你最懂",
- "让弹幕变得不一样",
- "弹幕世界大门已开",
- "字里行间,藏着宇宙的秘密",
- "在光与影的交织中,你的话语是唯一的真实",
- "有趣的灵魂万里挑一",
- "说点什么",
- "长期征集有趣的弹幕广告词",
- "广告位招租",
- "\uD83E\uDD14",
- "梦开始的地方",
- "心念成形",
- "發個彈幕炒熱氣氛!",
- "來個彈幕吧!",
- "發個友善的彈幕吧!",
- "是不是忍不住想發彈幕了呢?",
+ @Composable
+ private fun danmakuPlaceholders(): List = listOf(
+ stringResource(Lang.video_player_danmaku_placeholder_1),
+ stringResource(Lang.video_player_danmaku_placeholder_2),
+ stringResource(Lang.video_player_danmaku_placeholder_3),
+ stringResource(Lang.video_player_danmaku_placeholder_4),
+ stringResource(Lang.video_player_danmaku_placeholder_5),
+ stringResource(Lang.video_player_danmaku_placeholder_6),
+ stringResource(Lang.video_player_danmaku_placeholder_7),
+ stringResource(Lang.video_player_danmaku_placeholder_8),
+ stringResource(Lang.video_player_danmaku_placeholder_9),
+ stringResource(Lang.video_player_danmaku_placeholder_10),
+ stringResource(Lang.video_player_danmaku_placeholder_11),
+ stringResource(Lang.video_player_danmaku_placeholder_12),
+ stringResource(Lang.video_player_danmaku_placeholder_13),
+ stringResource(Lang.video_player_danmaku_placeholder_14),
+ stringResource(Lang.video_player_danmaku_placeholder_15),
+ stringResource(Lang.video_player_danmaku_placeholder_16),
+ stringResource(Lang.video_player_danmaku_placeholder_17),
+ stringResource(Lang.video_player_danmaku_placeholder_18),
+ stringResource(Lang.video_player_danmaku_placeholder_19),
+ stringResource(Lang.video_player_danmaku_placeholder_20),
)
- fun randomDanmakuPlaceholder(): String = DANMAKU_PLACEHOLDERS.random()
+ fun randomDanmakuPlaceholder(placeholders: List): String = placeholders.random()
+
+ @Composable
+ fun rememberRandomDanmakuPlaceholder(): String {
+ val placeholders = danmakuPlaceholders()
+ return remember(placeholders) { randomDanmakuPlaceholder(placeholders) }
+ }
/**
* To send danmaku
@@ -332,7 +350,7 @@ object PlayerControllerDefaults {
modifier: Modifier = Modifier,
) {
IconButton(onClick = onClick, enabled = enabled, modifier = modifier) {
- Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = "发送")
+ Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = stringResource(Lang.video_player_send))
}
}
@@ -367,7 +385,7 @@ object PlayerControllerDefaults {
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
placeholder: @Composable () -> Unit = {
Text(
- remember { randomDanmakuPlaceholder() },
+ rememberRandomDanmakuPlaceholder(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
@@ -486,7 +504,8 @@ object PlayerControllerDefaults {
renderValue = { Text(remember(it) { "${playbackSpeedControllerState.speedList[it]}x" }) },
renderValueExposed = {
val speedValue = playbackSpeedControllerState.speedList[it]
- Text(remember(speedValue) { if (speedValue == 1.0f) "倍速" else """${speedValue}x""" })
+ val speedText = stringResource(Lang.video_player_speed)
+ Text(remember(speedValue, speedText) { if (speedValue == 1.0f) speedText else """${speedValue}x""" })
},
modifier,
properties = PlatformPopupProperties(
@@ -622,9 +641,9 @@ object PlayerControllerDefaults {
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- Text("即将跳过 OP 或 ED")
+ Text(stringResource(Lang.video_player_skip_op_ed))
TextButton(onClick = onClick) {
- Text("取消")
+ Text(stringResource(Lang.video_player_cancel))
}
}
}
diff --git a/app/shared/video-player/src/commonMain/kotlin/ui/progress/SubtitleSwitcher.kt b/app/shared/video-player/src/commonMain/kotlin/ui/progress/SubtitleSwitcher.kt
index 5c3e937203..68f7631608 100644
--- a/app/shared/video-player/src/commonMain/kotlin/ui/progress/SubtitleSwitcher.kt
+++ b/app/shared/video-player/src/commonMain/kotlin/ui/progress/SubtitleSwitcher.kt
@@ -29,8 +29,10 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import me.him188.ani.app.ui.foundation.AbstractViewModel
import me.him188.ani.app.ui.foundation.dialogs.PlatformPopupProperties
+import me.him188.ani.app.ui.lang.*
import org.openani.mediamp.metadata.SubtitleTrack
import org.openani.mediamp.metadata.TrackGroup
+import org.jetbrains.compose.resources.*
@Stable
class SubtitleTrackState(
@@ -99,14 +101,15 @@ fun PlayerControllerDefaults.SubtitleSwitcher(
optionsProvider = { options },
renderValue = {
if (it == null) {
- Text("关闭")
+ Text(stringResource(Lang.video_player_off))
} else {
Text(it.displayName, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
},
renderValueExposed = {
+ val subtitleText = stringResource(Lang.video_player_subtitle)
Text(
- remember(it) { it?.displayName ?: "字幕" },
+ remember(it, subtitleText) { it?.displayName ?: subtitleText },
Modifier.widthIn(max = 64.dp),
maxLines = 1, overflow = TextOverflow.Ellipsis,
)