mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-23 09:17:06 +08:00
milestone: publish alpha version (#637)
- document folder - full-text search - blob storage - basic edgeless support Co-authored-by: tzhangchi <terry.zhangchi@outlook.com> Co-authored-by: QiShaoXuan <qishaoxuan777@gmail.com> Co-authored-by: DiamondThree <diamond.shx@gmail.com> Co-authored-by: MingLiang Wang <mingliangwang0o0@gmail.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Yifeng Wang <doodlewind@toeverything.info> Co-authored-by: Himself65 <himself65@outlook.com> Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com>
This commit is contained in:
66
packages/app/src/libs/i18n/index.ts
Normal file
66
packages/app/src/libs/i18n/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import i18next, { Resource } from 'i18next';
|
||||
import {
|
||||
I18nextProvider,
|
||||
initReactI18next,
|
||||
useTranslation,
|
||||
} from 'react-i18next';
|
||||
import { LOCALES } from './resources';
|
||||
import type en_US from './resources/en.json';
|
||||
|
||||
// const localStorage = {
|
||||
// getItem() {
|
||||
// return undefined;
|
||||
// },
|
||||
// setItem() {},
|
||||
// };
|
||||
// See https://react.i18next.com/latest/typescript
|
||||
declare module 'react-i18next' {
|
||||
interface CustomTypeOptions {
|
||||
// custom namespace type if you changed it
|
||||
// defaultNS: 'ns1';
|
||||
// custom resources type
|
||||
resources: {
|
||||
en: typeof en_US;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// const STORAGE_KEY = 'i18n_lng';
|
||||
|
||||
export { i18n, useTranslation, I18nProvider, LOCALES };
|
||||
|
||||
const resources = LOCALES.reduce<Resource>(
|
||||
(acc, { tag, res }) => ({ ...acc, [tag]: { translation: res } }),
|
||||
{}
|
||||
);
|
||||
|
||||
const fallbackLng = LOCALES[0].tag;
|
||||
const standardizeLocale = (language: string) => {
|
||||
if (LOCALES.find(locale => locale.tag === language)) return language;
|
||||
if (LOCALES.find(locale => locale.tag === language.slice(0, 2).toLowerCase()))
|
||||
return language;
|
||||
return fallbackLng;
|
||||
};
|
||||
|
||||
const language = standardizeLocale(
|
||||
// localStorage.getItem(STORAGE_KEY) ??
|
||||
// (typeof navigator !== 'undefined' ? navigator.language : 'en')
|
||||
'en'
|
||||
);
|
||||
|
||||
const i18n = i18next.createInstance();
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: language,
|
||||
fallbackLng,
|
||||
debug: false,
|
||||
resources,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
});
|
||||
|
||||
i18n.on('languageChanged', () => {
|
||||
// localStorage.setItem(STORAGE_KEY, lng);
|
||||
});
|
||||
|
||||
const I18nProvider = I18nextProvider;
|
||||
22
packages/app/src/libs/i18n/resources/bn.json
Normal file
22
packages/app/src/libs/i18n/resources/bn.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
|
||||
"Add A Below Block": "নীচে একটি ব্লক যোগ করুন",
|
||||
"WarningTips": {
|
||||
"IsNotfsApiSupported": "অ্যাফাইন ডেমোতে স্বাগতম। পরিবর্তনগুলি সংরক্ষণ করা শুরু করতে আপনি Chrome/Edge এর মতো ক্রোমিয়াম ভিত্তিক ব্রাউজারের সর্বশেষ সংস্করণের মাধ্যমে ডিস্কে ডেটা সিঙ্ক করতে পারেন",
|
||||
"DoNotStore": "অ্যাফাইন সক্রিয় ডেভেলপমেন্ট এর অধীনে এবং বর্তমান সংস্করণটি অস্থিতিশীল। দয়া করে কোন তথ্য বা ডেটা সঞ্চয় করবেন না"
|
||||
},
|
||||
"Language": "ভাষা",
|
||||
"Settings": "সেটিংস",
|
||||
"Share": "শেয়ার করুন",
|
||||
"Comment": "মন্তব্য",
|
||||
"Delete": "মুছে ফেলুন",
|
||||
"Copy Page Link": "পেজ লিংক কপি করুন",
|
||||
"Duplicate Page": "সদৃশ পৃষ্ঠা তৈরি করুন",
|
||||
"Logout": "লগআউট",
|
||||
"Divide Here As A New Group": "একটি নতুন গ্রুপ হিসেবে বিভক্ত করুন",
|
||||
"ComingSoon": "লেআউট সেটিংস শীঘ্রই আসছে...",
|
||||
"Clear Workspace": "ওয়ার্কস্পেস পরিষ্কার করুন",
|
||||
"Layout": "লেআউট",
|
||||
"Turn into": "রূপান্তর করুন",
|
||||
"Sync to Disk": "ডিস্ক এ সিঙ্ক করুন"
|
||||
}
|
||||
28
packages/app/src/libs/i18n/resources/en.json
Normal file
28
packages/app/src/libs/i18n/resources/en.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"Sync to Disk": "Sync to Disk",
|
||||
"Share": "Share",
|
||||
"WarningTips": {
|
||||
"IsNotfsApiSupported": "Welcome to the AFFiNE demo. To begin saving changes you can SYNC DATA TO DISK with the latest version of Chromium based browser like Chrome/Edge",
|
||||
"IsNotLocalWorkspace": "Welcome to the AFFiNE demo. To begin saving changes you can SYNC TO DISK.",
|
||||
"DoNotStore": "AFFiNE is under active development and the current version is UNSTABLE. Please DO NOT store information or data"
|
||||
},
|
||||
"Layout": "Layout",
|
||||
"Comment": "Comment",
|
||||
"Settings": "Settings",
|
||||
"ComingSoon": "Layout Settings Coming Soon...",
|
||||
"Duplicate Page": "Duplicate Page",
|
||||
"Copy Page Link": "Copy Page Link",
|
||||
"Language": "Language",
|
||||
"Clear Workspace": "Clear Workspace",
|
||||
"Export As Markdown": "Export As Markdown",
|
||||
"Export As HTML": "Export As HTML",
|
||||
"Export As PDF (Unsupported)": "Export As PDF (Unsupported)",
|
||||
"Import Workspace": "Import Workspace",
|
||||
"Export Workspace": "Export Workspace",
|
||||
"Last edited by": "Last edited by {{name}}",
|
||||
"Logout": "Logout",
|
||||
"Delete": "Delete",
|
||||
"Turn into": "Turn into",
|
||||
"Add A Below Block": "Add A Below Block",
|
||||
"Divide Here As A New Group": "Divide Here As A New Group"
|
||||
}
|
||||
29
packages/app/src/libs/i18n/resources/fr.json
Normal file
29
packages/app/src/libs/i18n/resources/fr.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
|
||||
"ComingSoon": "Bientôt disponible",
|
||||
"Duplicate Page": "Dupliquer la page",
|
||||
"Copy Page Link": "Copier le lien de la page",
|
||||
"Delete": "Supprimer",
|
||||
"Comment": "Commentaire",
|
||||
"Export As HTML": "Exporter en HTML",
|
||||
"Export As Markdown": "Exporter en Markdown",
|
||||
"Export As PDF (Unsupported)": "exporter en PDF (non supporté)",
|
||||
"Logout": "Déconnexion",
|
||||
"Export Workspace": "Exporter l'espace de travail",
|
||||
"Import Workspace": "Importer l'espace de travail",
|
||||
"Language": "Langue",
|
||||
"Last edited by": "Dernière édition par {{name}}",
|
||||
"Layout": "Mise en forme",
|
||||
"Settings": "Réglages",
|
||||
"Share": "Partager",
|
||||
"Sync to Disk": "Synchroniser sur le disque",
|
||||
"Turn into": "Transformer en",
|
||||
"WarningTips": {
|
||||
"DoNotStore": "Affine est en développement actif ; la version actuelle est INSTABLE. Veuillez NE PAS stocker d'informations ou de données",
|
||||
"IsNotLocalWorkspace": "Bienvenue sur la démo d'AFFiNE. Pour commencer à sauvegarder vos modifications, vous pouvez SYNCHRONISER SUR LE DISQUE",
|
||||
"IsNotfsApiSupported": "Bienvenue sur la démo d'AFFiNE. Pour commencer à sauvegarder vos modifications, vous pouvez SYNCHRONISER SUR LE DISQUE\navec la dernière version d'un navigateur basé sur Chromium tel que Chrome ou Edge."
|
||||
},
|
||||
"Add A Below Block": "Ajouter un bloc en-dessous",
|
||||
"Divide Here As A New Group": "Séparer ici en un nouveau groupe",
|
||||
"Clear Workspace": "Vider l'espace de travail"
|
||||
}
|
||||
72
packages/app/src/libs/i18n/resources/index.ts
Normal file
72
packages/app/src/libs/i18n/resources/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Run `pnpm run download-resources` to regenerate.
|
||||
// To overwrite this, please overwrite download.ts
|
||||
import en from './en.json';
|
||||
import zh_Hans from './zh-Hans.json';
|
||||
import zh_Hant from './zh-Hant.json';
|
||||
import sr from './sr.json';
|
||||
import fr from './fr.json';
|
||||
import bn from './bn.json';
|
||||
|
||||
export const LOCALES = [
|
||||
{
|
||||
id: 1000016008,
|
||||
name: 'English',
|
||||
tag: 'en',
|
||||
originalName: 'English',
|
||||
flagEmoji: '🇬🇧',
|
||||
base: true,
|
||||
completeRate: 1,
|
||||
res: en,
|
||||
},
|
||||
{
|
||||
id: 1000016009,
|
||||
name: 'Simplified Chinese',
|
||||
tag: 'zh-Hans',
|
||||
originalName: '简体中文',
|
||||
flagEmoji: '🇨🇳',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: zh_Hans,
|
||||
},
|
||||
{
|
||||
id: 1000016012,
|
||||
name: 'Traditional Chinese',
|
||||
tag: 'zh-Hant',
|
||||
originalName: '繁體中文',
|
||||
flagEmoji: '🇭🇰',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: zh_Hant,
|
||||
},
|
||||
{
|
||||
id: 1000034005,
|
||||
name: 'Serbian',
|
||||
tag: 'sr',
|
||||
originalName: 'српски',
|
||||
flagEmoji: '🇷🇸',
|
||||
base: false,
|
||||
completeRate: 0.9166666666666666,
|
||||
res: sr,
|
||||
},
|
||||
{
|
||||
id: 1000034008,
|
||||
name: 'French',
|
||||
tag: 'fr',
|
||||
originalName: 'français',
|
||||
flagEmoji: '🇫🇷',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: fr,
|
||||
},
|
||||
{
|
||||
id: 1000034010,
|
||||
name: 'Bangla',
|
||||
tag: 'bn',
|
||||
originalName: 'বাংলা',
|
||||
flagEmoji: '🇧🇩',
|
||||
base: false,
|
||||
completeRate: 0.7083333333333334,
|
||||
res: bn,
|
||||
},
|
||||
] as const;
|
||||
27
packages/app/src/libs/i18n/resources/sr.json
Normal file
27
packages/app/src/libs/i18n/resources/sr.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
|
||||
"Clear Workspace": "Očisti radni prostor",
|
||||
"ComingSoon": "Podešavanja za izgled dolaze",
|
||||
"Comment": "Komentar",
|
||||
"Copy Page Link": "Kopiraj link stranice",
|
||||
"Delete": "Obriši",
|
||||
"Duplicate Page": "Dupliraj stranicu",
|
||||
"Export As HTML": "Izvezi kao HTML",
|
||||
"Export As Markdown": "Izvezi kao Markdown",
|
||||
"Export As PDF (Unsupported)": "Izvezi kao PDF (nepodržano)",
|
||||
"Export Workspace": "Izvezi radnu površinu",
|
||||
"Import Workspace": "Poboljšaj radnu površinu",
|
||||
"Language": "Jezik",
|
||||
"Last edited by": "Zadnju promenu uradio {{ime}}",
|
||||
"Layout": "Izgled",
|
||||
"Logout": "Odjava",
|
||||
"Settings": "Podešavanja",
|
||||
"Share": "Podeli",
|
||||
"Sync to Disk": "Sinhroniziraj sa diskom",
|
||||
"Turn into": "Promeni u",
|
||||
"WarningTips": {
|
||||
"DoNotStore": "AFFiNE je u stanju aktivnog razvoja i trenutna verzija je NESTABILNA. Molimo vas, NEMOJTE čuvati informacije ili podatke.",
|
||||
"IsNotLocalWorkspace": "Dobrodošli u AFFiNE demo. Da bi započeli proces čuvanja promena možete kliknuti SINHRONIZUJ SA DISKOM.",
|
||||
"IsNotfsApiSupported": "Dobrodošli u AFFiNE demo. Da bi započeli proces čuvanja promena možete SINHRONIZOVATI NA DISK sa poslednjom verzijom pretraživača tipa Chromium, kao što su Chrome/Edge."
|
||||
}
|
||||
}
|
||||
29
packages/app/src/libs/i18n/resources/zh-Hans.json
Normal file
29
packages/app/src/libs/i18n/resources/zh-Hans.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
|
||||
"Sync to Disk": "同步到磁盘",
|
||||
"Share": "分享",
|
||||
"WarningTips": {
|
||||
"IsNotfsApiSupported": "欢迎来到AFFiNE 的演示界面。您可以使用最新版本的基于Chrome的浏览器(如Chrome/Edge)将数据同步到磁盘来进行保存",
|
||||
"IsNotLocalWorkspace": "欢迎来到AFFiNE 的演示界面,您可以同步到磁盘来进行保存操作。",
|
||||
"DoNotStore": "AFFiNE 正在积极开发中,当前版本不稳定。请不要存储信息或数据。"
|
||||
},
|
||||
"ComingSoon": "布局设置即将到来",
|
||||
"Layout": "布局",
|
||||
"Comment": "评论",
|
||||
"Settings": "设置",
|
||||
"Duplicate Page": "复制页面",
|
||||
"Copy Page Link": "复制页面链接",
|
||||
"Language": "当前语言",
|
||||
"Clear Workspace": "清空工作区域",
|
||||
"Export As Markdown": "导出 markdown",
|
||||
"Export As HTML": "导出 HTML",
|
||||
"Export As PDF (Unsupported)": "导出 PDF (暂不支持)",
|
||||
"Import Workspace": "导入 Workspace",
|
||||
"Export Workspace": "导出 Workspace",
|
||||
"Last edited by": "最后编辑者为 {{name}}",
|
||||
"Logout": "退出登录",
|
||||
"Delete": "删除",
|
||||
"Turn into": "转换为",
|
||||
"Add A Below Block": "在下方添加一个新块",
|
||||
"Divide Here As A New Group": "从这里划分一个新组"
|
||||
}
|
||||
29
packages/app/src/libs/i18n/resources/zh-Hant.json
Normal file
29
packages/app/src/libs/i18n/resources/zh-Hant.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
|
||||
"Add A Below Block": "在下方新添塊",
|
||||
"Clear Workspace": "清空工作區",
|
||||
"ComingSoon": "自定義佈局功能即將與您見面",
|
||||
"Comment": "評論",
|
||||
"Copy Page Link": "拷貝頁面鏈接",
|
||||
"Delete": "刪除",
|
||||
"Divide Here As A New Group": "從此地劃分成新組",
|
||||
"Duplicate Page": "複製界面",
|
||||
"Export As HTML": "導出 HTML",
|
||||
"Export As Markdown": "以 Markdown 導出",
|
||||
"Export As PDF (Unsupported)": "導出為 PDF(即將可用)",
|
||||
"Export Workspace": "導出 Workspace",
|
||||
"Import Workspace": "導入 Workspace",
|
||||
"Language": "語言",
|
||||
"Last edited by": "最後編輯者為 {{name}}",
|
||||
"Layout": "佈局",
|
||||
"Logout": "退出登錄",
|
||||
"Settings": "設置",
|
||||
"Share": "分享",
|
||||
"Sync to Disk": "同步到磁盤",
|
||||
"Turn into": "轉換為",
|
||||
"WarningTips": {
|
||||
"DoNotStore": "我們正在積極開發 AFFiNE,目前版本尚不穩定,請避免存儲信息或數據。",
|
||||
"IsNotLocalWorkspace": "歡迎來到 AFFiNE 演示界面。您可以通過「同步到磁盤」來保存更改。",
|
||||
"IsNotfsApiSupported": "歡迎進入AFFiNE演示!使用最新版本的基於 Chromium 內核的瀏覽器如Chrome/Edge,您可以通過「同步到磁盤」來保存更改"
|
||||
}
|
||||
}
|
||||
185
packages/app/src/libs/i18n/scripts/api.ts
Normal file
185
packages/app/src/libs/i18n/scripts/api.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
// cSpell:ignore Tolgee
|
||||
import { fetchTolgee } from './request';
|
||||
|
||||
/**
|
||||
* Returns all project languages
|
||||
*
|
||||
* See https://tolgee.io/api#operation/getAll_6
|
||||
* @example
|
||||
* ```ts
|
||||
* const languages = [
|
||||
* {
|
||||
* id: 1000016008,
|
||||
* name: 'English',
|
||||
* tag: 'en',
|
||||
* originalName: 'English',
|
||||
* flagEmoji: '🇬🇧',
|
||||
* base: true
|
||||
* },
|
||||
* {
|
||||
* id: 1000016013,
|
||||
* name: 'Spanish',
|
||||
* tag: 'es',
|
||||
* originalName: 'español',
|
||||
* flagEmoji: '🇪🇸',
|
||||
* base: false
|
||||
* },
|
||||
* {
|
||||
* id: 1000016009,
|
||||
* name: 'Simplified Chinese',
|
||||
* tag: 'zh-Hans',
|
||||
* originalName: '简体中文',
|
||||
* flagEmoji: '🇨🇳',
|
||||
* base: false
|
||||
* },
|
||||
* {
|
||||
* id: 1000016012,
|
||||
* name: 'Traditional Chinese',
|
||||
* tag: 'zh-Hant',
|
||||
* originalName: '繁體中文',
|
||||
* flagEmoji: '🇭🇰',
|
||||
* base: false
|
||||
* }
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export const getAllProjectLanguages = async (size = 1000) => {
|
||||
const url = `/languages?size=${size}`;
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json: {
|
||||
_embedded: {
|
||||
languages: {
|
||||
id: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
originalName: string;
|
||||
flagEmoji: string;
|
||||
base: boolean;
|
||||
}[];
|
||||
};
|
||||
page: unknown;
|
||||
} = await resp.json();
|
||||
return json._embedded.languages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns translations in project
|
||||
*
|
||||
* See https://tolgee.io/api#operation/getTranslations_
|
||||
*/
|
||||
export const getTranslations = async () => {
|
||||
const url = '/translations';
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all translations for specified languages
|
||||
*
|
||||
* See https://tolgee.io/api#operation/getAllTranslations_1
|
||||
*/
|
||||
export const getLanguagesTranslations = async <T extends string>(
|
||||
languages: T
|
||||
) => {
|
||||
const url = `/translations/${languages}`;
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json: { [key in T]?: Record<string, string> } = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
export const getRemoteTranslations = async (languages: string) => {
|
||||
const translations = await getLanguagesTranslations(languages);
|
||||
if (!(languages in translations)) {
|
||||
return {};
|
||||
}
|
||||
// The assert is safe because we checked above
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return translations[languages]!;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates new key
|
||||
*
|
||||
* See https://tolgee.io/api#operation/create_2
|
||||
*/
|
||||
export const createsNewKey = async (
|
||||
key: string,
|
||||
translations: Record<string, string>
|
||||
) => {
|
||||
const url = '/translations/keys/create';
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: key, translations }),
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tags a key with tag. If tag with provided name doesn't exist, it is created
|
||||
*
|
||||
* See https://tolgee.io/api#operation/tagKey_1
|
||||
*/
|
||||
export const addTag = async (keyId: string, tagName: string) => {
|
||||
const url = `/keys/${keyId}/tags`;
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: tagName }),
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tags a key with tag. If tag with provided name doesn't exist, it is created
|
||||
*
|
||||
* See https://tolgee.io/api#operation/tagKey_1
|
||||
*/
|
||||
export const removeTag = async (keyId: string, tagId: number) => {
|
||||
const url = `/keys/${keyId}/tags/${tagId}`;
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
// export const addTagByKey = async (key: string, tag: string) => {
|
||||
// // TODO get key id by key name
|
||||
// // const keyId =
|
||||
// // addTag(keyId, tag);
|
||||
// };
|
||||
|
||||
/**
|
||||
* Exports data
|
||||
*
|
||||
* See https://tolgee.io/api#operation/export_1
|
||||
*/
|
||||
export const exportResources = async () => {
|
||||
const url = `/export`;
|
||||
const resp = await fetchTolgee(url);
|
||||
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
132
packages/app/src/libs/i18n/scripts/download.ts
Normal file
132
packages/app/src/libs/i18n/scripts/download.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
// cSpell:ignore Tolgee
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { format } from 'prettier';
|
||||
import { getAllProjectLanguages, getRemoteTranslations } from './api';
|
||||
import type { TranslationRes } from './utils';
|
||||
|
||||
const RES_DIR = path.resolve(process.cwd(), 'src', 'resources');
|
||||
|
||||
const countKeys = (obj: TranslationRes) => {
|
||||
let count = 0;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Object.entries(obj).forEach(([_, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
count++;
|
||||
} else {
|
||||
count += countKeys(value);
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
const getBaseTranslations = async (baseLanguage: { tag: string }) => {
|
||||
try {
|
||||
const baseTranslationsStr = await fs.readFile(
|
||||
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
const baseTranslations = JSON.parse(baseTranslationsStr);
|
||||
return baseTranslations;
|
||||
} catch (e) {
|
||||
console.error('base language:', JSON.stringify(baseLanguage));
|
||||
console.error('Failed to read base language', e);
|
||||
const translations = await getRemoteTranslations(baseLanguage.tag);
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
||||
JSON.stringify(translations, null, 4)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log('Loading project languages...');
|
||||
const languages = await getAllProjectLanguages();
|
||||
const baseLanguage = languages.find(language => language.base);
|
||||
if (!baseLanguage) {
|
||||
console.error(JSON.stringify(languages));
|
||||
throw new Error('Could not find base language');
|
||||
}
|
||||
console.log(`Loading ${baseLanguage.tag} languages translations as base...`);
|
||||
|
||||
const baseTranslations = await getBaseTranslations(baseLanguage);
|
||||
const baseKeyNum = countKeys(baseTranslations);
|
||||
const languagesWithTranslations = await Promise.all(
|
||||
languages.map(async language => {
|
||||
console.log(`Loading ${language.tag} translations...`);
|
||||
const translations = await getRemoteTranslations(language.tag);
|
||||
const keyNum = countKeys(translations);
|
||||
|
||||
return {
|
||||
...language,
|
||||
translations,
|
||||
completeRate: keyNum / baseKeyNum,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const availableLanguages = languagesWithTranslations.filter(
|
||||
language => language.completeRate > 0
|
||||
);
|
||||
|
||||
availableLanguages
|
||||
// skip base language
|
||||
.filter(i => !i.base)
|
||||
.forEach(async language => {
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, `${language.tag}.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.':
|
||||
'',
|
||||
...language.translations,
|
||||
},
|
||||
null,
|
||||
4
|
||||
) + '\n'
|
||||
);
|
||||
});
|
||||
|
||||
console.log('Generating meta data...');
|
||||
const code = `// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Run \`pnpm run download-resources\` to regenerate.
|
||||
// To overwrite this, please overwrite ${path.basename(__filename)}
|
||||
${availableLanguages
|
||||
.map(
|
||||
language =>
|
||||
`import ${language.tag.replaceAll('-', '_')} from './${
|
||||
language.tag
|
||||
}.json'`
|
||||
)
|
||||
.join('\n')}
|
||||
|
||||
export const LOCALES = [
|
||||
${availableLanguages
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.map(({ translations, ...language }) =>
|
||||
JSON.stringify({
|
||||
...language,
|
||||
res: '__RES_PLACEHOLDER',
|
||||
}).replace(
|
||||
'"__RES_PLACEHOLDER"',
|
||||
language.tag.replaceAll('-', '_')
|
||||
)
|
||||
)
|
||||
.join(',\n')}
|
||||
] as const;
|
||||
`;
|
||||
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, 'index.ts'),
|
||||
format(code, {
|
||||
parser: 'typescript',
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
tabWidth: 4,
|
||||
arrowParens: 'avoid',
|
||||
})
|
||||
);
|
||||
console.log('Done');
|
||||
};
|
||||
|
||||
main();
|
||||
55
packages/app/src/libs/i18n/scripts/request.ts
Normal file
55
packages/app/src/libs/i18n/scripts/request.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// cSpell:ignore Tolgee
|
||||
const TOLGEE_API_KEY = process.env['TOLGEE_API_KEY'];
|
||||
const TOLGEE_API_URL = 'https://i18n.affine.pro';
|
||||
|
||||
if (!TOLGEE_API_KEY) {
|
||||
throw new Error(`Please set "TOLGEE_API_KEY" as environment variable!`);
|
||||
}
|
||||
|
||||
const withTolgee = (
|
||||
fetch: typeof globalThis.fetch
|
||||
): typeof globalThis.fetch => {
|
||||
const baseUrl = `${TOLGEE_API_URL}/v2/projects`;
|
||||
const headers = new Headers({
|
||||
'X-API-Key': TOLGEE_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
const isRequest = (input: RequestInfo | URL): input is Request => {
|
||||
return typeof input === 'object' && !('href' in input);
|
||||
};
|
||||
|
||||
return new Proxy(fetch, {
|
||||
apply(
|
||||
target,
|
||||
thisArg: unknown,
|
||||
argArray: Parameters<typeof globalThis.fetch>
|
||||
) {
|
||||
if (isRequest(argArray[0])) {
|
||||
// Request
|
||||
if (!argArray[0].headers) {
|
||||
argArray[0] = {
|
||||
...argArray[0],
|
||||
url: `${baseUrl}${argArray[0].url}`,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// URL or URLLike + ?RequestInit
|
||||
if (typeof argArray[0] === 'string') {
|
||||
argArray[0] = `${baseUrl}${argArray[0]}`;
|
||||
}
|
||||
if (!argArray[1]) {
|
||||
argArray[1] = {};
|
||||
}
|
||||
if (!argArray[1].headers) {
|
||||
argArray[1].headers = headers;
|
||||
}
|
||||
}
|
||||
// console.log('fetch', argArray);
|
||||
return target.apply(thisArg, argArray);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchTolgee = withTolgee(globalThis.fetch);
|
||||
154
packages/app/src/libs/i18n/scripts/sync.ts
Normal file
154
packages/app/src/libs/i18n/scripts/sync.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
// cSpell:ignore Tolgee
|
||||
import { readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { createsNewKey, getRemoteTranslations } from './api';
|
||||
import type { TranslationRes } from './utils';
|
||||
|
||||
const BASE_JSON_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'src',
|
||||
'resources',
|
||||
'en.json'
|
||||
);
|
||||
const BASE_LANGUAGES = 'en' as const;
|
||||
|
||||
/**
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* flatRes({ a: { b: 'c' } }); // { 'a.b': 'c' }
|
||||
* ```
|
||||
*/
|
||||
const flatRes = (obj: TranslationRes) => {
|
||||
const getEntries = (o: TranslationRes, prefix = ''): [string, string][] =>
|
||||
Object.entries(o).flatMap<[string, string]>(([k, v]) =>
|
||||
typeof v !== 'string'
|
||||
? getEntries(v, `${prefix}${k}.`)
|
||||
: [[`${prefix}${k}`, v]]
|
||||
);
|
||||
return Object.fromEntries(getEntries(obj));
|
||||
};
|
||||
|
||||
const differenceObject = (
|
||||
newObj: Record<string, string>,
|
||||
oldObj: Record<string, string>
|
||||
) => {
|
||||
const add: string[] = [];
|
||||
const remove: string[] = [];
|
||||
const modify: string[] = [];
|
||||
const both: string[] = [];
|
||||
|
||||
Object.keys(newObj).forEach(key => {
|
||||
if (!(key in oldObj)) {
|
||||
add.push(key);
|
||||
} else {
|
||||
both.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(oldObj).forEach(key => {
|
||||
if (!(key in newObj)) {
|
||||
remove.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
both.forEach(key => {
|
||||
if (!(key in newObj) || !(key in oldObj)) {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
const newVal = newObj[key];
|
||||
const oldVal = oldObj[key];
|
||||
if (newVal !== oldVal) {
|
||||
modify.push(key);
|
||||
}
|
||||
});
|
||||
return { add, remove, modify };
|
||||
};
|
||||
|
||||
function warnDiff(diff: { add: string[]; remove: string[]; modify: string[] }) {
|
||||
if (diff.add.length) {
|
||||
console.log('New keys found:', diff.add.join(', '));
|
||||
//See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
|
||||
process.env['CI'] &&
|
||||
console.log(
|
||||
`::notice file=${BASE_JSON_PATH},line=1,title=New keys::${diff.add.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (diff.remove.length) {
|
||||
console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', '));
|
||||
process.env['CI'] &&
|
||||
console.warn(
|
||||
`::notice file=${BASE_JSON_PATH},line=1,title=Unused keys::${diff.remove.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (diff.modify.length) {
|
||||
console.warn('[WARN]', 'Inconsistent keys found:', diff.modify.join(', '));
|
||||
process.env['CI'] &&
|
||||
console.warn(
|
||||
`::warning file=${BASE_JSON_PATH},line=1,title=Inconsistent keys::${diff.modify.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
console.log('Loading local base translations...');
|
||||
const baseLocalTranslations = JSON.parse(
|
||||
await readFile(BASE_JSON_PATH, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
);
|
||||
const flatLocalTranslations = flatRes(baseLocalTranslations);
|
||||
console.log(
|
||||
`Loading local base translations success! Total ${
|
||||
Object.keys(flatLocalTranslations).length
|
||||
} keys`
|
||||
);
|
||||
|
||||
console.log('Fetch remote base translations...');
|
||||
const baseRemoteTranslations = await getRemoteTranslations(BASE_LANGUAGES);
|
||||
const flatRemoteTranslations = flatRes(baseRemoteTranslations);
|
||||
console.log(
|
||||
`Fetch remote base translations success! Total ${
|
||||
Object.keys(flatRemoteTranslations).length
|
||||
} keys`
|
||||
);
|
||||
|
||||
const diff = differenceObject(flatLocalTranslations, flatRemoteTranslations);
|
||||
|
||||
console.log(''); // new line
|
||||
warnDiff(diff);
|
||||
console.log(''); // new line
|
||||
|
||||
if (process.argv.slice(2).includes('--check')) {
|
||||
// check mode
|
||||
return;
|
||||
}
|
||||
|
||||
diff.add.forEach(async key => {
|
||||
const val = flatLocalTranslations[key];
|
||||
console.log(`Creating new key: ${key} -> ${val}`);
|
||||
await createsNewKey(key, { [BASE_LANGUAGES]: val });
|
||||
});
|
||||
|
||||
// TODO remove unused tags from used keys
|
||||
|
||||
// diff.remove.forEach(key => {
|
||||
// // TODO set unused tag
|
||||
// // console.log(`Add ${DEPRECATED_TAG_NAME} to ${key}`);
|
||||
// addTagByKey(key, DEPRECATED_TAG_NAME);
|
||||
// });
|
||||
|
||||
// diff.modify.forEach(key => {
|
||||
// // TODO warn different between local and remote base translations
|
||||
// });
|
||||
|
||||
// TODO send notification
|
||||
};
|
||||
|
||||
main();
|
||||
3
packages/app/src/libs/i18n/scripts/utils.ts
Normal file
3
packages/app/src/libs/i18n/scripts/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface TranslationRes {
|
||||
[x: string]: string | TranslationRes;
|
||||
}
|
||||
Reference in New Issue
Block a user