chore: seed migration objects batch 17

This commit is contained in:
dev
2026-10-18 13:16:01 +08:00
parent d1d50c0746
commit 7d84a81a6f
16 changed files with 623 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 591 KiB

+14
View File
@@ -0,0 +1,14 @@
# 监控 Tinode 服务器
Tinode 服务器可以选择在可配置的 HTTP(S) 端点将运行时统计信息公开为 JSON 文档。该功能通过在配置文件中添加字符串参数 `expvar` 启用。`expvar` 的值是变量发布位置的 URL 路径。除了配置文件,该功能也可以从命令行添加 `--expvar` 参数启用。如果 `expvar` 的值为空字符串 `""` 或短横线 `"-"`,则禁用该功能。命令行参数的非空值会覆盖配置文件值。
默认配置文件中启用该功能,在 `/debug/vars` 发布统计信息。
截至撰写时,发布以下统计信息:
* `memstats`: Go 的内存统计,如 https://golang.org/pkg/runtime/#MemStats 所述
* `cmdline`: 服务器的命令行参数,以字符串数组形式。
* `TotalSessions`: 服务器生命周期内创建的所有会话计数。
* `LiveSessions`: 当前存活的会话数,无论认证状态。
* `TotalTopics`: 服务器生命周期内激活的所有话题计数。
* `LiveTopics`: 当前活跃的话题数。
+14
View File
@@ -0,0 +1,14 @@
# Monitoring Tinode server
Tinode server can optionally expose runtime statistics as a json document at a configurable HTTP(S) endpoint. The feature is enabled by adding a string parameter `expvar` to the config file. The value of the `expvar` is the URL path pointing to the location where the variables are served. In addition to the config file, the feature can be enabled from the command line by adding an `--expvar` parameter. The feature is disabled if the value of `expvar` is an empty string `""` or a dash `"-"`. A non-blank value of the command line parameter overrides the config file value.
The feature is enabled in the default config file to publish stats at `/debug/vars`.
As of the time of this writing the following stats are published:
* `memstats`: Go's memory statistics as described at https://golang.org/pkg/runtime/#MemStats
* `cmdline`: server's command line parameters as an array of strings.
* `TotalSessions`: the count of all sessions which were created during server's life time.
* `LiveSessions`: the number of sessions currently live, regardless of authentication status.
* `TotalTopics`: the count of all topics activated during servers's life time.
* `LiveTopics`: the number of currently active topics.
@@ -0,0 +1,327 @@
-- MySQL multi-tenant schema migration draft.
-- REVIEW ONLY: do not execute before the application is fully tenant-aware.
-- Target: MySQL 8.0, final schema version 119.
--
-- This migration assigns all existing data to the default tenant, then makes
-- tenant_id mandatory and replaces global uniqueness/foreign keys with
-- tenant-scoped constraints.
--
-- MySQL DDL auto-commits. Back up the database and rehearse this migration on
-- a production-sized copy before running it in any environment.
-- ---------------------------------------------------------------------------
-- 1. Tenant registry and default tenant.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS im_tenant (
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
`desc` VARCHAR(256) NULL,
state SMALLINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_im_tenant_code (code)
) ENGINE=InnoDB;
INSERT INTO im_tenant (code, name, `desc`, state)
SELECT 'default', 'Default Tenant', NULL, 1
WHERE NOT EXISTS (
SELECT 1 FROM im_tenant WHERE code = 'default'
);
SET @default_tenant_id := (
SELECT id
FROM im_tenant
WHERE code = 'default'
LIMIT 1
);
-- Stop immediately if the default tenant could not be resolved.
DELIMITER $$
CREATE PROCEDURE assert_default_tenant()
BEGIN
IF @default_tenant_id IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'default tenant is missing';
END IF;
END$$
DELIMITER ;
CALL assert_default_tenant();
DROP PROCEDURE assert_default_tenant;
-- ---------------------------------------------------------------------------
-- 2. Add nullable tenant_id columns first.
-- ---------------------------------------------------------------------------
ALTER TABLE users
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE usertags
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE devices
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE auth
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE topics
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE topictags
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE subscriptions
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE messages
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE dellog
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE credentials
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE fileuploads
ADD COLUMN tenant_id BIGINT NULL AFTER id;
ALTER TABLE filemsglinks
ADD COLUMN tenant_id BIGINT NULL AFTER id;
-- ---------------------------------------------------------------------------
-- 3. Backfill all existing rows into the default tenant.
-- ---------------------------------------------------------------------------
UPDATE users SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE usertags SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE devices SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE auth SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE topics SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE topictags SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE subscriptions SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE messages SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE dellog SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE credentials SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE fileuploads SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
UPDATE filemsglinks SET tenant_id = @default_tenant_id WHERE tenant_id IS NULL;
-- Validate the backfill before adding NOT NULL constraints.
DELIMITER $$
CREATE PROCEDURE assert_no_null_tenant_id()
BEGIN
IF EXISTS (SELECT 1 FROM users WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM usertags WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM devices WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM auth WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM topics WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM topictags WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM subscriptions WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM messages WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM dellog WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM credentials WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM fileuploads WHERE tenant_id IS NULL LIMIT 1)
OR EXISTS (SELECT 1 FROM filemsglinks WHERE tenant_id IS NULL LIMIT 1)
THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'tenant_id backfill is incomplete';
END IF;
END$$
DELIMITER ;
CALL assert_no_null_tenant_id();
DROP PROCEDURE assert_no_null_tenant_id;
-- ---------------------------------------------------------------------------
-- 4. Make tenant_id mandatory.
-- No DEFAULT is retained: new code must always provide a tenant explicitly.
-- ---------------------------------------------------------------------------
ALTER TABLE users MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE usertags MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE devices MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE auth MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE topics MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE topictags MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE subscriptions MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE messages MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE dellog MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE credentials MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE fileuploads MODIFY COLUMN tenant_id BIGINT NOT NULL;
ALTER TABLE filemsglinks MODIFY COLUMN tenant_id BIGINT NOT NULL;
-- ---------------------------------------------------------------------------
-- 5. Drop existing single-column foreign keys.
-- Constraint names can differ between databases, so resolve them dynamically.
-- ---------------------------------------------------------------------------
DELIMITER $$
CREATE PROCEDURE drop_table_foreign_keys(IN table_name_value VARCHAR(64))
BEGIN
DECLARE drop_clauses LONGTEXT;
SELECT GROUP_CONCAT(
CONCAT('DROP FOREIGN KEY `', constraint_name, '`')
ORDER BY constraint_name
SEPARATOR ', '
)
INTO drop_clauses
FROM information_schema.table_constraints
WHERE constraint_schema = DATABASE()
AND table_name = table_name_value
AND constraint_type = 'FOREIGN KEY';
IF drop_clauses IS NOT NULL AND drop_clauses <> '' THEN
SET @drop_fk_sql = CONCAT(
'ALTER TABLE `', table_name_value, '` ', drop_clauses
);
PREPARE drop_fk_stmt FROM @drop_fk_sql;
EXECUTE drop_fk_stmt;
DEALLOCATE PREPARE drop_fk_stmt;
END IF;
END$$
DELIMITER ;
CALL drop_table_foreign_keys('usertags');
CALL drop_table_foreign_keys('devices');
CALL drop_table_foreign_keys('auth');
CALL drop_table_foreign_keys('topictags');
CALL drop_table_foreign_keys('subscriptions');
CALL drop_table_foreign_keys('messages');
CALL drop_table_foreign_keys('dellog');
CALL drop_table_foreign_keys('credentials');
CALL drop_table_foreign_keys('filemsglinks');
DROP PROCEDURE drop_table_foreign_keys;
-- ---------------------------------------------------------------------------
-- 6. Replace global unique indexes with tenant-scoped unique indexes.
-- Parent identity indexes are also added for composite foreign keys.
-- ---------------------------------------------------------------------------
ALTER TABLE users
ADD UNIQUE KEY uk_users_tenant_id_id (tenant_id, id),
ADD KEY idx_users_tenant_state_stateat (tenant_id, state, stateat),
ADD KEY idx_users_tenant_lastseen_updatedat (tenant_id, lastseen, updatedat);
ALTER TABLE usertags
ADD UNIQUE KEY uk_usertags_tenant_user_tag (tenant_id, userid, tag),
ADD KEY idx_usertags_tenant_tag (tenant_id, tag),
DROP INDEX usertags_userid_tag;
ALTER TABLE devices
ADD UNIQUE KEY uk_devices_tenant_hash (tenant_id, hash),
ADD KEY idx_devices_tenant_user (tenant_id, userid),
DROP INDEX devices_hash;
ALTER TABLE auth
ADD UNIQUE KEY uk_auth_tenant_user_scheme (tenant_id, userid, scheme),
ADD UNIQUE KEY uk_auth_tenant_uname (tenant_id, uname),
DROP INDEX auth_userid_scheme,
DROP INDEX auth_uname;
ALTER TABLE topics
ADD UNIQUE KEY uk_topics_tenant_name (tenant_id, name),
ADD KEY idx_topics_tenant_owner (tenant_id, owner),
ADD KEY idx_topics_tenant_state_stateat (tenant_id, state, stateat),
ADD KEY idx_topics_tenant_name_state_seqid (tenant_id, name, state, seqid),
DROP INDEX topics_name;
ALTER TABLE topictags
ADD UNIQUE KEY uk_topictags_tenant_topic_tag (tenant_id, topic, tag),
ADD KEY idx_topictags_tenant_tag (tenant_id, tag),
DROP INDEX topictags_topic_tag;
ALTER TABLE subscriptions
ADD UNIQUE KEY uk_subscriptions_tenant_topic_user (tenant_id, topic, userid),
ADD KEY idx_subscriptions_tenant_user_topic_deleted
(tenant_id, userid, topic, deletedat),
ADD KEY idx_subscriptions_tenant_topic_deleted
(tenant_id, topic, deletedat),
DROP INDEX subscriptions_topic_userid;
ALTER TABLE messages
ADD UNIQUE KEY uk_messages_tenant_id (tenant_id, id),
ADD UNIQUE KEY uk_messages_tenant_topic_seqid (tenant_id, topic, seqid),
DROP INDEX messages_topic_seqid;
ALTER TABLE dellog
ADD KEY idx_dellog_tenant_topic_delid_user
(tenant_id, topic, delid, deletedfor),
ADD KEY idx_dellog_tenant_topic_user_low_hi
(tenant_id, topic, deletedfor, low, hi),
ADD KEY idx_dellog_tenant_deletedfor (tenant_id, deletedfor);
ALTER TABLE credentials
ADD UNIQUE KEY uk_credentials_tenant_synthetic (tenant_id, synthetic),
ADD KEY idx_credentials_tenant_user_method
(tenant_id, userid, method, deletedat),
DROP INDEX credentials_uniqueness;
ALTER TABLE fileuploads
ADD UNIQUE KEY uk_fileuploads_tenant_id (tenant_id, id),
ADD KEY idx_fileuploads_tenant_status_updated
(tenant_id, status, updatedat);
ALTER TABLE filemsglinks
ADD KEY idx_filemsglinks_tenant_file (tenant_id, fileid),
ADD KEY idx_filemsglinks_tenant_message (tenant_id, msgid),
ADD KEY idx_filemsglinks_tenant_topic (tenant_id, topic),
ADD KEY idx_filemsglinks_tenant_user (tenant_id, userid);
-- ---------------------------------------------------------------------------
-- 7. Add tenant registry and tenant-consistent parent foreign keys.
-- ---------------------------------------------------------------------------
-- ---------------------------------------------------------------------------
-- 8. Validate tenant consistency after constraints are installed.
-- All result counts must be zero.
-- ---------------------------------------------------------------------------
SELECT 'user_tags_without_same_tenant_user' AS check_name, COUNT(*) AS invalid_count
FROM usertags child
LEFT JOIN users parent
ON parent.tenant_id = child.tenant_id AND parent.id = child.userid
WHERE parent.id IS NULL
UNION ALL
SELECT 'subscriptions_without_same_tenant_user', COUNT(*)
FROM subscriptions child
LEFT JOIN users parent
ON parent.tenant_id = child.tenant_id AND parent.id = child.userid
WHERE parent.id IS NULL
UNION ALL
SELECT 'subscriptions_without_same_tenant_topic', COUNT(*)
FROM subscriptions child
LEFT JOIN topics parent
ON parent.tenant_id = child.tenant_id AND parent.name = child.topic
WHERE parent.name IS NULL
UNION ALL
SELECT 'messages_without_same_tenant_topic', COUNT(*)
FROM messages child
LEFT JOIN topics parent
ON parent.tenant_id = child.tenant_id AND parent.name = child.topic
WHERE parent.name IS NULL
UNION ALL
SELECT 'file_links_without_same_tenant_file', COUNT(*)
FROM filemsglinks child
LEFT JOIN fileuploads parent
ON parent.tenant_id = child.tenant_id AND parent.id = child.fileid
WHERE parent.id IS NULL;
-- ---------------------------------------------------------------------------
-- 9. Update the schema version only after all validation succeeds.
-- ---------------------------------------------------------------------------
UPDATE kvmeta
SET `value` = '119'
WHERE `key` = 'version';
-- Intentionally unchanged:
-- * im_tenant: it is the tenant registry itself.
-- * kvmeta: it stores platform-level schema metadata. Tenant-scoped cache
-- data must use a separate table or a mandatory tenant key namespace.
View File
+64
View File
@@ -0,0 +1,64 @@
# theCard: 人物/话题描述格式
Tinode 使用 `theCard` 存储和传输人物和话题的描述。该格式概念上类似于 [vCard](https://www.rfc-editor.org/rfc/rfc6350.txt) 3.0。
当使用 `JSON` 表示 `theCard` 数据时,其方式与 [jCard](https://tools.ietf.org/html/rfc7095) 不同。`theCard` 和 `jCard` 不兼容。主要区别是 `theCard` 使用对象表示逻辑相关数据,而 `jCard` 使用有序数组。
`theCard` 结构为对象:
```js
{
fn: "John Doe", // 字符串,人物或话题的格式化名称。
photo: { // 对象,头像照片;必须有 'data' 或 'ref',其他字段可选。
type: "jpeg", // 字符串,MIME 类型但去掉 'image/'。
data: "Rt53jUU...iVBORw0KGgoA==", // 字符串,base64 编码的二进制图片数据
ref: "https://api.tinode.co/file/s/abcdef12345.jpg", // 字符串,图片 URL。
width: 512, // 整数,图片宽度(像素)。
height: 512, // 整数,图片高度(像素)。
size: 123456 // 整数,图片大小(字节)。
},
note: "Some notes", // 字符串,人物或话题的描述。
//
// 以下字段目前没有任何已知客户端实现:
//
n: { // 对象,人物的结构化名称。
surname: "Miner", // 姓氏或姓或家族名。
given: "Coal", // 名字或名。
additional: "Diamond", // 额外名称,如中间名或父名。
prefix: "Dr.", // 前缀,如荣誉头衔或性别标识。
suffix: "Jr.", // 后缀,如 'Jr' 或 'II'。
},
org: { // 对象,人物或话题所属的组织。
fn: "Most Evil Corp", // 字符串,组织的格式化名称。
title: "CEO", // 字符串,人物在组织中的职位。
},
comm: [ // 对象数组,定义与人物或话题的通信方式。
{
des: ["home", "voice"], // 联系标识,可选。
proto: "tel", // 通信协议,必需
value: "+17025551234" // 电话号码。
},
{
des: ["work"],
proto: "email",
value: "alice@example.com", // 电子邮件地址
},
{
des: ["other"],
proto: "tinode",
value: "tinode:topic/usrRkDVe0PYDOo", // tinode ID URI,可包含服务器地址。
},
{
proto: "http", // 应用于 http 或 https 网站地址。
value: "https://tinode.co", // 网站实际地址。
}, ...
],
bday: { // 对象,人物生日。
y: 1970, // 整数,年
m: 1, // 整数,月 1..12
d: 15 // 整数,日 1..31
},
}
```
所有字段都是可选的。Tinode 客户端目前只使用 `fn`、`photo`、`org`、`note`、`comm` 字段。如果将来需要其他字段,将从相应的 [vCard](https://www.rfc-editor.org/rfc/rfc6350.txt) 字段采用。
+64
View File
@@ -0,0 +1,64 @@
# theCard: Person/Topic Description Format
Tinode uses `theCard` to store and transmit descriptions of people and topics. The format is conceptually similar to [vCard](https://www.rfc-editor.org/rfc/rfc6350.txt) 3.0.
When `JSON` is used to represent `theCard` data, it does it differently than [jCard](https://tools.ietf.org/html/rfc7095). `theCard` and `jCard` are incompatible. The main difference is that `theCard` uses objects to represent logically related data while `jCard` uses ordered arrays.
`theCard` is structured as an object:
```js
{
fn: "John Doe", // string, formatted name of the person or topic.
photo: { // object, avatar photo; either 'data' or 'ref' must be present, all other fields are optional.
type: "jpeg", // string, MIME type but with 'image/' dropped.
data: "Rt53jUU...iVBORw0KGgoA==", // string, base64-encoded binary image data
ref: "https://api.tinode.co/file/s/abcdef12345.jpg", // string, URL of the image.
width: 512, // integer, image width in pixels.
height: 512, // integer, image height in pixels.
size: 123456 // integer, image size in bytes.
},
note: "Some notes", // string, description of a person or a topic.
//
// None of the following fields are implemented by any known client:
//
n: { // object, person's structured name.
surname: "Miner", // surname or last or family name.
given: "Coal", // first or given name.
additional: "Diamond", // additional name, such as middle name or patronymic.
prefix: "Dr.", // prefix, such as honorary title or gender designation.
suffix: "Jr.", // suffix, such as 'Jr' or 'II'.
},
org: { // object, organization the person or topic belongs to.
fn: "Most Evil Corp", // string, formatted name of the organisation.
title: "CEO", // string, person's job title at the organisation.
},
comm: [ // array of objects defining means of communication with the the person or topic.
{
des: ["home", "voice"], // contact designation, optional.
proto: "tel", // communication protocol, required
value: "+17025551234" // phone number.
},
{
des: ["work"],
proto: "email",
value: "alice@example.com", // email address
},
{
des: ["other"],
proto: "tinode",
value: "tinode:topic/usrRkDVe0PYDOo", // tinode ID URI, may include server address.
},
{
proto: "http", // should be used for either http or https website addresses.
value: "https://tinode.co", // actual address of a website.
}, ...
],
bday: { // object, person's birthday.
y: 1970, // integer, year
m: 1, // integer, month 1..12
d: 15 // integer, day 1..31
},
}
```
All fields are optional. Tinode clients currently use only `fn`, `photo`, `org`, `note`, `comm` fields. If other fields are needed in the future, then they will be adopted from the correspondent [vCard](https://www.rfc-editor.org/rfc/rfc6350.txt) fields.
+70
View File
@@ -0,0 +1,70 @@
# Tinode 本地化
**重要!** 请使用 `devel` 分支进行翻译。
## 服务器
服务器在新账户创建时和用户请求重置密码时向用户发送电子邮件或短信:
* [/server/templ/email-validation-en.templ](../server/templ/email-validation-en.templ)
* [/server/templ/email-password-reset-en.templ](../server/templ/email-password-reset-en.templ)
* [/server/templ/sms-validation-en.templ](../server/templ/sms-validation-en.templ)
创建文件副本,命名为 `email-password-reset-XX.templ`、`email-validation-XX.templ`、`sms-validation-XX.templ`,其中 `XX` 是新语言的 [ISO-631-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 代码。翻译内容并通过新文件发送拉取请求。如果您不知道如何创建拉取请求,可以任何方式发送翻译后的文件。
## Webapp
翻译位于两个位置:[/src/i18n/](https://github.com/tinode/webapp/tree/devel/src/i18n/) 和 [/service-worker.js](https://github.com/tinode/webapp/blob/devel/service-worker.js#L11)。
要添加翻译,将 `/src/i18n/en.json` 复制到名为 `/src/i18n/XX.json` 的文件,其中 `XX` 是新语言的 [BCP-47](https://tools.ietf.org/rfc/bcp/bcp47.txt) 代码。如果不确定如何选择 BCP-47 语言代码,请使用两位字母的 [ISO-631-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)。只需翻译 `"translation":` 行,不应翻译 `"defaultMessage"`、`"description"` 等,它们仅作为帮助:
```js
"action_block_contact": {
"translation": "Bloquear contacto", // <<<---- 只需翻译此字符串
"defaultMessage": "Block Contact", // 这是英文默认消息
"description": "Flat button [Block Contact]", // 这是字符串使用位置/方式的说明
"missing": false,
"obsolete": false
},
```
翻译 `service-worker.js` 时,直接将字符串添加到文件中。只需翻译两个字符串 "New message" 和 "New chat":
```js
const i18n = {
...
'XX': {
'new_message': "New message",
'new_chat': "New chat",
},
...
```
请通过新文件发送拉取请求。如果您不知道如何创建拉取请求,可以任何方式发送文件。
## Android
需要翻译单个文件:[/tinode/tindroid/app/src/main/res/values/strings.xml](https://github.com/tinode/tindroid/blob/devel/app/src/main/res/values/strings.xml)
在 [app/src/main/res](https://github.com/tinode/tindroid/tree/devel/app/src/main/res) 创建新目录 `values-XX`,其中 `XX` 是两位字母的 [ISO-631-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 代码,可选后跟两位字母的 [ISO 3166-1-alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) 区域代码(以小写 r 开头)。例如 `values-pt.xml` 包含葡萄牙语翻译,而 `values-pt-rBR.xml` 用于_巴西_葡萄牙语翻译。
复制英文字符串文件的副本,放到新目录。翻译所有未标记 `translatable="false"` 的字符串(标记 `translatable="false"` 的字符串根本不需要包含),然后通过新文件发送拉取请求。如果您不知道如何创建拉取请求,可以任何方式发送文件。
## iOS
不幸的是,iOS 本地化过程非常复杂,通常需要只在 Mac 上运行的 `Xcode`。除非您熟悉 iOS 开发,请为所需语言创建[功能请求](https://github.com/tinode/ios/issues/new?assignees=&labels=&template=feature_request.md&title=),我们会发送文件供您翻译。
如果您足够勇敢,可以翻译[以下 .xliff 文件](https://github.com/tinode/ios/blob/devel/Localizations/en.xcloc/Localized%20Contents/en.xliff)然后以任何方式发送给我们。翻译 `<target>` 标签之间的所有字符串:
```xml
<trans-unit id="Action failed: %@" xml:space="preserve"> <!-- 不要更改此行 -->
<source>Action failed: %@</source> <!-- 这是英文默认消息 -->
<target>Se ha producido un error al realizar la acción: %@</target> <!-- 只有此字符串 "target" 需要翻译 -->
<note>Toast notification</note> <!-- 这是字符串使用位置/方式的说明 -->
</trans-unit>
```
如果您熟悉 Xcode 和 iOS 本地化,导出的本地化位于 [/Localizations](https://github.com/tinode/ios/tree/devel/Localizations)。
+70
View File
@@ -0,0 +1,70 @@
# Localizing Tinode
**IMPORTANT!** Please use `devel` branches for translations.
## Server
The server sends emails or SMS to users upon creation of a new account and when the user requests to reset the password:
* [/server/templ/email-validation-en.templ](../server/templ/email-validation-en.templ)
* [/server/templ/email-password-reset-en.templ](../server/templ/email-password-reset-en.templ)
* [/server/templ/sms-validation-en.templ](../server/templ/sms-validation-en.templ)
Create a copy of the files naming them `email-password-reset-XX.teml`, `email-validation-XX.templ`, `sms-validation-XX.templ` where `XX` is the [ISO-631-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code of the new language. Translate the content and send a pull request with the new files. If you don't know how to create a pull request then just sent the translated files in any way you can.
## Webapp
The translations are located in two places: [/src/i18n/](https://github.com/tinode/webapp/tree/devel/src/i18n/) and [/service-worker.js](https://github.com/tinode/webapp/blob/devel/service-worker.js#L11).
In order to add a translation, copy `/src/i18n/en.json` to a file named `/src/i18n/XX.json` where `XX` is the [BCP-47](https://tools.ietf.org/rfc/bcp/bcp47.txt) code of the new language. If in doubt how to choose the BCP-47 language code, use a two letter [ISO-631-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). Only the `"translation":` line has to be translated, the `"defaultMessage"`, `"description"` etc should NOT be translated, they serve as help only, i.e.:
```js
"action_block_contact": {
"translation": "Bloquear contacto", // <<<---- Only this string needs to be translated
"defaultMessage": "Block Contact", // This is the default message in English
"description": "Flat button [Block Contact]", // This is an explanation where/how the string is used.
"missing": false,
"obsolete": false
},
```
When translating the `service-worker.js`, just add the strings directly to the file. Only two strings need to be translated "New message" and "New chat":
```js
const i18n = {
...
'XX': {
'new_message': "New message",
'new_chat': "New chat",
},
...
```
Please send a pull request with the new files. If you don't know how to create a pull request just sent the files in any way you can.
## Android
A single file needs to be translated: [/tinode/tindroid/app/src/main/res/values/strings.xml](https://github.com/tinode/tindroid/blob/devel/app/src/main/res/values/strings.xml)
Create a new directory `values-XX` in [app/src/main/res](https://github.com/tinode/tindroid/tree/devel/app/src/main/res), where `XX` is a two-letter [ISO-631-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code , optionally followed by a two letter [ISO 3166-1-alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) region code (preceded by lowercase r). For example `values-pt.xml` would contain Portuguese translations while `values-pt-rBR.xml` is for _Brazilian_ Portuguese translations.
Make a copy of the file with English strings, place it to the new directory. Translate all the strings not marked with `translatable="false"` (the strings with `translatable="false"` don't need to be included at all) then send a pull request with the new file. If you don't know how to create a pull request then just sent the file in any way you can.
## iOS
Unfortunately iOS localization process is very convoluted and generally requires `Xcode` which runs only on Mac. Unless you are familiar with the iOS development, please create a [feature request](https://github.com/tinode/ios/issues/new?assignees=&labels=&template=feature_request.md&title=) for the desired language and we will send you the file for translation.
If you feel brave enough, you can translate the [following .xliff file](https://github.com/tinode/ios/blob/devel/Localizations/en.xcloc/Localized%20Contents/en.xliff) and then send it to us in any way you can. Translate all the strings between the `<target>` tags:
```xml
<trans-unit id="Action failed: %@" xml:space="preserve"> <!-- Do NOT change this line -->
<source>Action failed: %@</source> <!-- This is the default message in English. -->
<target>Se ha producido un error al realizar la acción: %@</target> <!-- Only this string "target" needs to be translated. -->
<note>Toast notification</note> <!-- This is an explanation where/how the string is used. -->
</trans-unit>
```
If you are familiar with Xcode and localization for iOS, the exported localizations are located at [/Localizations](https://github.com/tinode/ios/tree/devel/Localizations).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 637 KiB