feat(locale): allows to manage locale resources in core package (#2293)

* feat(locale): add app.locales

* chore: change directory

* chore: change locale directories

* fix: test

* fix: cached resources changed after sync

* chore: change fr-FR locale directory
This commit is contained in:
YANG QIA 2023-07-25 17:09:34 +08:00 committed by GitHub
parent e27c72e8b0
commit 45bc0b83ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 326 additions and 403 deletions

View File

@ -16,4 +16,4 @@
"directory": "packages/actions"
},
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
}
}

View File

@ -19,4 +19,4 @@
"directory": "packages/resourcer"
},
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
}
}

View File

@ -31,4 +31,4 @@
"@types/semver": "^7.3.9"
},
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
}
}

View File

@ -18,6 +18,7 @@ import { createACL } from './acl';
import { AppManager } from './app-manager';
import { registerCli } from './commands';
import { createI18n, createResourcer, registerMiddlewares } from './helper';
import { Locale } from './locale';
import { Plugin } from './plugin';
import { InstallOptions, PluginManager } from './plugin-manager';
@ -167,6 +168,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
protected _authManager: AuthManager;
protected _locales: Locale;
protected _version: ApplicationVersion;
protected plugins = new Map<string, Plugin>();
@ -230,6 +233,10 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this._logger;
}
get locales() {
return this._locales;
}
get name() {
return this.options.name || 'main';
}
@ -298,6 +305,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this._resourcer.use(this._acl.middleware(), { tag: 'acl', after: ['auth'] });
}
this._locales = new Locale(this);
registerMiddlewares(this, options);
if (options.registerActions !== false) {

View File

@ -0,0 +1 @@
export * from './locale';

View File

@ -0,0 +1,68 @@
import { Cache, createCache } from '@nocobase/cache';
import { lodash } from '@nocobase/utils';
import Application from '../application';
import { PluginManager } from '../plugin-manager';
import { getAntdLocale } from './antd';
import { getCronstrueLocale } from './cronstrue';
import { getResource } from './resource';
export class Locale {
app: Application;
cache: Cache;
defaultLang = 'en-US';
constructor(app: Application) {
this.app = app;
this.cache = createCache();
this.app.on('afterLoad', () => this.load());
}
load() {
this.getCacheResources(this.defaultLang);
}
async get(lang: string) {
return {
antd: await this.wrapCache(`locale:antd:${lang}`, () => getAntdLocale(lang)),
cronstrue: await this.wrapCache(`locale:cronstrue:${lang}`, () => getCronstrueLocale(lang)),
resources: await this.getCacheResources(lang),
};
}
async wrapCache(key: string, fn: () => any) {
const result = await this.cache.get(key);
if (result) {
return result;
}
const value = await fn();
if (lodash.isEmpty(value)) {
return value;
}
await this.cache.set(key, value);
return value;
}
async getCacheResources(lang: string) {
return await this.wrapCache(`locale:resources:${lang}`, () => this.getResources(lang));
}
getResources(lang: string) {
const resources = {};
const plugins = this.app.pm.getPlugins();
for (const name of plugins.keys()) {
try {
const packageName = PluginManager.getPackageName(name);
const res = getResource(packageName, lang);
if (res) {
resources[name] = { ...res };
}
} catch (err) {}
}
const res = getResource('@nocobase/client', lang);
if (res) {
resources['client'] = { ...(resources['client'] || {}), ...res };
}
return resources;
}
}

View File

@ -0,0 +1,27 @@
const arr2obj = (items: any[]) => {
const obj = {};
for (const item of items) {
Object.assign(obj, item);
}
return obj;
};
export const getResource = (packageName: string, lang: string) => {
const resources = [];
const prefixes = ['src', 'lib'];
for (const prefix of prefixes) {
try {
const file = `${packageName}/${prefix}/locale/${lang}`;
require.resolve(file);
const resource = require(file).default;
resources.push(resource);
} catch (error) {}
if (resources.length) {
break;
}
}
if (resources.length === 0 && lang.replace('-', '_') !== lang) {
return getResource(packageName, lang.replace('-', '_'));
}
return arr2obj(resources);
};

View File

@ -16,6 +16,7 @@ const locale = {
'7 Days': '7 天',
'30 Days': '30 天',
'90 Days': '90 天',
'Role not found': '角色不存在',
};
export default locale;

View File

@ -1,10 +0,0 @@
const locale = {
'Auth Type': '认证类型',
Authenticators: '认证器',
Authentication: '用户认证',
'Sign in via email': '邮箱登录',
'Not allowed to sign up': '禁止注册',
'Allow to sign up': '允许注册',
};
export default locale;

View File

@ -0,0 +1,17 @@
const locale = {
'Auth Type': '认证类型',
Authenticators: '认证器',
Authentication: '用户认证',
'Sign in via email': '邮箱登录',
'Not allowed to sign up': '禁止注册',
'Allow to sign up': '允许注册',
'The email is incorrect, please re-enter': '邮箱有误,请重新输入',
'Please fill in your email address': '请填写邮箱',
'The password is incorrect, please re-enter': '密码有误,请重新输入',
'Not a valid cellphone number, please re-enter': '不是有效的手机号,请重新输入',
'The phone number has been registered, please login directly': '手机号已注册,请直接登录',
'The phone number is not registered, please register first': '手机号未注册,请先注册',
'Please keep and enable at least one authenticator': '请至少保留并启用一个认证器',
};
export default locale;

View File

@ -1,23 +0,0 @@
export default {
Edit: 'Modifier',
Delete: 'Supprimer',
Cancel: 'Annuler',
Submit: 'Envoyer',
Actions: 'Actions',
Title: 'Titre',
Enable: 'Activer',
'SAML manager': 'SAML manager',
'SAML Providers': 'SAML Providers',
'Redirect url': 'URL de redirection',
'SP entity id': 'SP entity id',
'Add provider': 'Ajouter',
'Edit provider': 'Modifier',
'Client id': 'Client id',
'Entity id or issuer': 'Entity id or issuer',
'Login Url': 'URL de connexion',
'Public cert': 'Public cert',
'Delete provider': 'Supprimer',
'Are you sure you want to delete it?': 'Êtes-vous sûr de vouloir le supprimer ?',
'Sign in button name, which will be displayed on the sign in page':
'Nom du bouton de connexion, qui sera affiché sur la page de connexion',
};

View File

@ -1 +1 @@
export { default, getResourceLocale } from './server';
export { default } from './server';

View File

@ -1,2 +1 @@
export { getResourceLocale } from './resource';
export { default } from './server';

View File

@ -1,141 +0,0 @@
const locales = {
af: 'af',
'ar-dz': 'ar-dz',
'ar-kw': 'ar-kw',
'ar-ly': 'ar-ly',
'ar-ma': 'ar-ma',
'ar-sa': 'ar-sa',
'ar-tn': 'ar-tn',
ar: 'ar',
az: 'az',
be: 'be',
bg: 'bg',
bm: 'bm',
'bn-bd': 'bn-bd',
bn: 'bn',
bo: 'bo',
br: 'br',
bs: 'bs',
ca: 'ca',
cs: 'cs',
cv: 'cv',
cy: 'cy',
da: 'da',
'de-at': 'de-at',
'de-ch': 'de-ch',
de: 'de',
dv: 'dv',
el: 'el',
'en-au': 'en-au',
'en-ca': 'en-ca',
'en-gb': 'en-gb',
'en-ie': 'en-ie',
'en-il': 'en-il',
'en-in': 'en-in',
'en-nz': 'en-nz',
'en-sg': 'en-sg',
eo: 'eo',
'es-do': 'es-do',
'es-mx': 'es-mx',
'es-us': 'es-us',
es: 'es',
et: 'et',
eu: 'eu',
fa: 'fa',
fi: 'fi',
fil: 'fil',
fo: 'fo',
'fr-ca': 'fr-ca',
'fr-ch': 'fr-ch',
fr: 'fr',
fy: 'fy',
ga: 'ga',
gd: 'gd',
gl: 'gl',
'gom-deva': 'gom-deva',
'gom-latn': 'gom-latn',
gu: 'gu',
he: 'he',
hi: 'hi',
hr: 'hr',
hu: 'hu',
'hy-am': 'hy-am',
id: 'id',
is: 'is',
'it-ch': 'it-ch',
it: 'it',
'ja-JP': 'ja',
jv: 'jv',
ka: 'ka',
kk: 'kk',
km: 'km',
kn: 'kn',
ko: 'ko',
ku: 'ku',
ky: 'ky',
lb: 'lb',
lo: 'lo',
lt: 'lt',
lv: 'lv',
me: 'me',
mi: 'mi',
mk: 'mk',
ml: 'ml',
mn: 'mn',
mr: 'mr',
'ms-my': 'ms-my',
ms: 'ms',
mt: 'mt',
my: 'my',
nb: 'nb',
ne: 'ne',
'nl-be': 'nl-be',
nl: 'nl',
nn: 'nn',
'oc-lnc': 'oc-lnc',
'pa-in': 'pa-in',
pl: 'pl',
'pt-br': 'pt-br',
pt: 'pt',
ro: 'ro',
'ru-RU': 'ru',
sd: 'sd',
se: 'se',
si: 'si',
sk: 'sk',
sl: 'sl',
sq: 'sq',
'sr-cyrl': 'sr-cyrl',
sr: 'sr',
ss: 'ss',
sv: 'sv',
sw: 'sw',
ta: 'ta',
te: 'te',
tet: 'tet',
tg: 'tg',
'th-TH': 'th',
tk: 'tk',
'tl-ph': 'tl-ph',
tlh: 'tlh',
'tr-TR': 'tr',
tzl: 'tzl',
'tzm-latn': 'tzm-latn',
tzm: 'tzm',
'ug-cn': 'ug-cn',
uk: 'uk',
ur: 'ur',
'uz-latn': 'uz-latn',
uz: 'uz',
vi: 'vi',
'x-pseudo': 'x-pseudo',
yo: 'yo',
'zh-CN': 'zh-cn',
'zh-hk': 'zh-hk',
'zh-mo': 'zh-mo',
'zh-TW': 'zh-tw',
};
export const getMomentLocale = (lang: string) => {
return locales[lang] || 'en';
};

View File

@ -1,69 +0,0 @@
import { PluginManager } from '@nocobase/server';
const arr2obj = (items: any[]) => {
const obj = {};
for (const item of items) {
Object.assign(obj, item);
}
return obj;
};
const getResource = (packageName: string, lang: string) => {
const resources = [];
const prefixes = ['src', 'lib'];
const localeKeys = ['locale', 'client/locale', 'server/locale'];
for (const prefix of prefixes) {
for (const localeKey of localeKeys) {
try {
const file = `${packageName}/${prefix}/${localeKey}/${lang}`;
require.resolve(file);
const resource = require(file).default;
resources.push(resource);
} catch (error) {}
}
if (resources.length) {
break;
}
}
if (resources.length === 0 && lang.replace('-', '_') !== lang) {
return getResource(packageName, lang.replace('-', '_'));
}
return arr2obj(resources);
};
export const getResourceLocale = async (lang: string, db: any) => {
const resources = {};
const res = getResource('@nocobase/client', lang);
const defaults = getResource('@nocobase/client', 'zh-CN');
for (const key in defaults) {
if (Object.prototype.hasOwnProperty.call(defaults, key)) {
defaults[key] = key;
}
}
if (res) {
resources['client'] = { ...defaults, ...res };
} else {
resources['client'] = defaults;
}
const plugins = await db.getRepository('applicationPlugins').find({
filter: {
'name.$ne': 'client',
},
});
for (const plugin of plugins) {
const packageName = PluginManager.getPackageName(plugin.get('name'));
const res = getResource(packageName, lang);
const defaults = getResource(packageName, 'zh-CN');
for (const key in defaults) {
if (Object.prototype.hasOwnProperty.call(defaults, key)) {
defaults[key] = key;
}
}
if (res) {
resources[plugin.get('name')] = { ...defaults, ...res };
} else {
resources['client'] = defaults;
}
}
return resources;
};

View File

@ -1,14 +1,8 @@
import { Plugin, PluginManager } from '@nocobase/server';
import { lodash } from '@nocobase/utils';
import fs from 'fs';
import send from 'koa-send';
import serve from 'koa-static';
import { isAbsolute, resolve } from 'path';
import { getAntdLocale } from './antd';
import { getCronLocale } from './cron';
import { getCronstrueLocale } from './cronstrue';
import { getMomentLocale } from './moment-locale';
import { getResourceLocale } from './resource';
async function getReadMe(name: string, locale: string) {
const packageName = PluginManager.getPackageName(name);
@ -128,7 +122,6 @@ export class ClientPlugin extends Plugin {
actions: ['app:reboot', 'app:clearCache'],
});
const dialect = this.app.db.sequelize.getDialect();
const locales = require('./locale').default;
const restartMark = resolve(process.cwd(), 'storage', 'restart');
this.app.on('beforeStart', async () => {
if (fs.existsSync(restartMark)) {
@ -159,25 +152,10 @@ export class ClientPlugin extends Plugin {
},
async getLang(ctx, next) {
const lang = await getLang(ctx);
if (lodash.isEmpty(locales[lang])) {
locales[lang] = {};
}
if (lodash.isEmpty(locales[lang].resources)) {
locales[lang].resources = await getResourceLocale(lang, ctx.db);
}
if (lodash.isEmpty(locales[lang].antd)) {
locales[lang].antd = getAntdLocale(lang);
}
if (lodash.isEmpty(locales[lang].cronstrue)) {
locales[lang].cronstrue = getCronstrueLocale(lang);
}
if (lodash.isEmpty(locales[lang].cron)) {
locales[lang].cron = getCronLocale(lang);
}
const resources = await ctx.app.locales.get(lang);
ctx.body = {
lang,
moment: getMomentLocale(lang),
...locales[lang],
...resources,
};
await next();
},

View File

@ -0,0 +1,8 @@
export default {
'Select Import data': '请选择导入数据',
'Select Import Plugins': '请选择导入插件',
'Select User Collections': '请选择用户数据',
'Basic Data': '基础数据',
'Optional Data': '可选数据',
'User Data': '用户数据',
};

View File

@ -0,0 +1,6 @@
export default {
'unique violation': '{{field}} must be unique',
'notNull violation': 'notNull violation',
'Validation error': '{{field}} validation error',
'notNull Violation': '{{field}} cannot be null',
};

View File

@ -0,0 +1,6 @@
export default {
"unique violation": "{{field}} debe ser único",
"notNull violation": "notNull violación",
"Validation error": "{{field}} error de validación",
"notNull Violation": "{{field}} no puede ser null"
};

View File

@ -0,0 +1,6 @@
export default {
'unique violation': '{{field}} doit être unique',
'notNull violation': 'Violation de contrainte notNull',
'Validation error': 'Erreur de validation de {{field}}',
'notNull Violation': '{{field}} ne peut pas être null',
};

View File

@ -0,0 +1,4 @@
export default {
'unique violation': '{{field}} は一意でなくてはなりません',
'notNull Violation': '{{field}} はNullにできません',
};

View File

@ -0,0 +1,6 @@
export default {
'unique violation': '{{field}} deve ser único',
'notNull violation': 'violação de não nulo',
'Validation error': 'erro de validação de {{field}}',
'notNull Violation': '{{field}} não pode ser nulo',
};

View File

@ -0,0 +1,5 @@
export default {
'unique violation': '{{field}} 字段值是唯一的',
'notNull violation': '{{field}} 字段不能为空',
'Validation error': '{{field}} 字段规则验证失败',
};

View File

@ -1,21 +1,21 @@
export default {
'File manager': 'File manager',
'Attachment': 'Attachment',
Attachment: 'Attachment',
'MIME type': 'MIME type',
'Storage display name': 'Storage display name',
'Storage name': 'Storage name',
'Storage type': 'Storage type',
'Default storage': 'Default storage',
'Storage base URL': 'Storage base URL',
'Destination': 'Destination',
Destination: 'Destination',
'Use the built-in static file server': 'Use the built-in static file server',
'Local storage': 'Local storage',
'Aliyun OSS': 'Aliyun OSS',
'Tencent COS': 'Tencent COS',
'Amazon S3': 'Amazon S3',
'Region': 'Region',
'Bucket': 'Bucket',
'Path': 'Path',
'Filename': 'Filename',
Region: 'Region',
Bucket: 'Bucket',
Path: 'Path',
Filename: 'Filename',
'Will be used for API': 'Will be used for API',
};

View File

@ -1,21 +1,21 @@
export default {
'File manager': 'Gestionnaire de fichiers',
'Attachment': 'Pièce jointe',
Attachment: 'Pièce jointe',
'MIME type': 'Type MIME',
'Storage display name': 'Nom d\'affichage du stockage',
'Storage display name': "Nom d'affichage du stockage",
'Storage name': 'Nom du stockage',
'Storage type': 'Type de stockage',
'Default storage': 'Stockage par défaut',
'Storage base URL': 'URL de base du stockage',
'Destination': 'Destination',
Destination: 'Destination',
'Use the built-in static file server': 'Utiliser le serveur de fichiers statique intégré',
'Local storage': 'Stockage local',
'Aliyun OSS': 'Aliyun OSS',
'Tencent COS': 'Tencent COS',
'Amazon S3': 'Amazon S3',
'Region': 'Region',
'Bucket': 'Bucket',
'Path': 'Chemin',
'Filename': 'Nom de fichier',
'Will be used for API': 'Sera utilisé pour l\'API',
Region: 'Region',
Bucket: 'Bucket',
Path: 'Chemin',
Filename: 'Nom de fichier',
'Will be used for API': "Sera utilisé pour l'API",
};

View File

@ -1,3 +1,3 @@
export { default as enUS } from './en-US';
export { default as zhCN } from './zh-CN';
export { default as jaJP } from './ja-JP';
// export { default as enUS } from './en-US';
// export { default as zhCN } from './zh-CN';
// export { default as jaJP } from './ja-JP';

View File

@ -5,11 +5,11 @@ export default {
'Collection Search': 'Recherche de collection',
'Create Collection': 'Créer une collection',
'All Fields': 'Tous les champs',
'Association Fields': 'Champs d\'association',
'Association Fields': "Champs d'association",
'Choices fields': 'Champs de choix',
'All relationships': 'Toutes les relations',
'Entity relationship only': 'Uniquement les relations d\'entité',
'Inheritance relationship only': 'Uniquement les relations d\'héritage',
'Entity relationship only': "Uniquement les relations d'entité",
'Inheritance relationship only': "Uniquement les relations d'héritage",
'Graphical interface': 'Interface graphique',
'Selection': 'Sélection',
Selection: 'Sélection',
};

View File

@ -1,2 +1,2 @@
export { default as enUS } from './en-US';
export { default as zhCN } from './zh-CN';
// export { default as enUS } from './en-US';
// export { default as zhCN } from './zh-CN';

View File

@ -20,4 +20,11 @@ export default {
Yes: '是',
No: '否',
'Field {{fieldName}} does not exist': '字段 {{fieldName}} 不存在',
'can not find value': '找不到对应值',
'password is empty': '密码为空',
'Incorrect time format': '时间格式不正确',
'Incorrect date format': '日期格式不正确',
'Incorrect email format': '邮箱格式不正确',
'Illegal percentage format': '百分比格式有误',
'Imported template does not match, please download again.': '导入模板不匹配,请检查导入文件标题行或重新下载导入模板',
};

View File

@ -18,4 +18,4 @@
"displayName.zh-CN": "多语言管理",
"description": "Allows to manage localization resources of the application.",
"description.zh-CN": "支持管理应用程序的多语言资源。"
}
}

View File

@ -1,6 +1,5 @@
import { Context, Next } from '@nocobase/actions';
import { Database, Model, Op } from '@nocobase/database';
import { getResourceLocale } from '@nocobase/plugin-client';
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
import LocalizationManagementPlugin from '../plugin';
import { getTextsFromDBRecord, getTextsFromUISchema } from '../utils';
@ -10,8 +9,8 @@ const getResourcesInstance = async (ctx: Context) => {
return plugin.resources;
};
export const getResources = async (locale: string, db: Database) => {
const resources = await getResourceLocale(locale, db);
export const getResources = async (ctx: Context) => {
const resources = await ctx.app.locales.getCacheResources(ctx.get('X-Locale') || 'en-US');
const client = resources['client'];
// Remove duplicated keys
Object.keys(resources).forEach((module) => {
@ -24,7 +23,7 @@ export const getResources = async (locale: string, db: Database) => {
}
});
});
return resources;
return { ...resources };
};
export const getUISchemas = async (db: Database) => {
@ -174,7 +173,7 @@ const sync = async (ctx: Context, next: Next) => {
let resources: { [module: string]: any } = { client: {} };
if (type.includes('local')) {
resources = await getResources(locale, ctx.db);
resources = await getResources(ctx);
}
if (type.includes('menu')) {
const menuTexts = await getTextsFromMenu(ctx.db);

View File

@ -1,5 +1,3 @@
const locale = {
}
const locale = {};
export default locale;

View File

@ -1,5 +1,3 @@
const locale = {
}
const locale = {};
export default locale;

View File

@ -1,9 +0,0 @@
export default {
"Multi-app manager": "Gestor de aplicaciones múltiples",
"Applications": "Aplicaciones",
"App display name": "Mostrar nombre de aplicación",
"App ID": "ID de aplicación",
"Pin to menu": " Fijar al menú",
"Custom domain": "Dominio personalizado",
"Manage applications": "Gestionar aplicaciones"
};

View File

@ -0,0 +1,9 @@
export default {
'Multi-app manager': 'Gestor de aplicaciones múltiples',
Applications: 'Aplicaciones',
'App display name': 'Mostrar nombre de aplicación',
'App ID': 'ID de aplicación',
'Pin to menu': ' Fijar al menú',
'Custom domain': 'Dominio personalizado',
'Manage applications': 'Gestionar aplicaciones',
};

View File

@ -1,13 +0,0 @@
export default {
"Share collections": "Tablas compartidas",
"Unshared collections": "Tablas no compartidas",
"Shared collections": "Tablas compartidas",
"All categories": "Todas las categorías",
"Enter name or title...": "Introducir nombre o título...",
"Are you sure to add the following collections?": "¿Está seguro de que desea añadir las siguientes tablas?",
"Are you sure to remove the following collections?": "¿Está seguro de que desea eliminar las siguientes tablas?",
"Collection display name": "Mostrar nombre de la tabla",
"Collection name": "Nombre de la tabla",
"Collection category": "Categoría de tabla"
};

View File

@ -0,0 +1,12 @@
export default {
'Share collections': 'Tablas compartidas',
'Unshared collections': 'Tablas no compartidas',
'Shared collections': 'Tablas compartidas',
'All categories': 'Todas las categorías',
'Enter name or title...': 'Introducir nombre o título...',
'Are you sure to add the following collections?': '¿Está seguro de que desea añadir las siguientes tablas?',
'Are you sure to remove the following collections?': '¿Está seguro de que desea eliminar las siguientes tablas?',
'Collection display name': 'Mostrar nombre de la tabla',
'Collection name': 'Nombre de la tabla',
'Collection category': 'Categoría de tabla',
};

View File

@ -1,23 +0,0 @@
export default {
Edit: 'Modifier',
Delete: 'Supprimer',
Cancel: 'Annuler',
Submit: 'Envoyer',
Actions: 'Actions',
Title: 'Titre',
Enable: 'Activer',
'SAML manager': 'SAML manager',
'SAML Providers': 'SAML Providers',
'Redirect url': 'Url de redirection',
'SP entity id': 'SP entity id',
'Add provider': 'Ajouter',
'Edit provider': 'Modifier',
'Client id': 'Client id',
'Entity id or issuer': 'Entity id or issuer',
'Login Url': 'Url de connexion',
'Public cert': 'Public cert',
'Delete provider': 'Supprimer',
'Are you sure you want to delete it?': 'Êtes-vous sûr de vouloir le supprimer ?',
'Sign in button name, which will be displayed on the sign in page':
'Nom du bouton de connexion, qui sera affiché sur la page de connexion',
};

Some files were not shown because too many files have changed in this diff Show More