support language translation.
This commit is contained in:
10
src/common/kintoneLanguageDetector.ts
Normal file
10
src/common/kintoneLanguageDetector.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { LanguageDetectorModule } from 'i18next';
|
||||
|
||||
const KintoneLanguageDetector: LanguageDetectorModule = {
|
||||
type: 'languageDetector',
|
||||
// init: () => {},
|
||||
detect: () => kintone.getLoginUser().language,
|
||||
// cacheUserLanguage: () => {},
|
||||
};
|
||||
|
||||
export default KintoneLanguageDetector;
|
@@ -1,9 +0,0 @@
|
||||
import invariant from 'tiny-invariant';
|
||||
|
||||
const KintoneUserLanguages = ['en', 'ja', 'zh', 'zh-TW', 'es', 'pt-BR', 'th'] as const;
|
||||
export type KintoneUserLanguages = (typeof KintoneUserLanguages)[number];
|
||||
export const LANGUAGE = kintone.getLoginUser().language as KintoneUserLanguages;
|
||||
invariant(
|
||||
KintoneUserLanguages.includes(LANGUAGE),
|
||||
`Unsupported language: ${LANGUAGE}. Supported languages are: ${KintoneUserLanguages.join(', ')}`,
|
||||
);
|
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
|
||||
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||
import moize from 'moize';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { naturalCompare } from '../common/stringUtils';
|
||||
import { KintoneFormFieldProperties } from '../common/types';
|
||||
@@ -18,7 +19,7 @@ import styles from './Settings.module.css';
|
||||
|
||||
const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise<KintoneFormFieldProperties> => {
|
||||
const client = new KintoneRestAPIClient();
|
||||
const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false });
|
||||
const { properties } = await client.app.getFormFields({ app: appId, preview: true });
|
||||
return properties;
|
||||
});
|
||||
|
||||
@@ -28,6 +29,7 @@ interface SettingsProps {
|
||||
|
||||
const Settings: React.FC<SettingsProps> = (props) => {
|
||||
const { pluginId } = props;
|
||||
const { t } = useTranslation();
|
||||
const config = kintone.plugin.app.getConfig(pluginId);
|
||||
const appId = kintone.app.getId();
|
||||
invariant(appId, 'The app ID is not available. Please ensure you are on a Kintone app page.');
|
||||
@@ -36,7 +38,7 @@ const Settings: React.FC<SettingsProps> = (props) => {
|
||||
.filter((property) => property.type === 'FILE')
|
||||
.sort((a, b) => naturalCompare(`${a.label} (${a.code})`, `${b.label} (${b.code})`));
|
||||
const options: KintonePluginSelectOptionData[] = [
|
||||
{ value: '', label: 'Select a File field', disabled: true }, // Default option
|
||||
{ value: '', label: t('settings.template.messages.select-an-attachment-fields'), disabled: true }, // Default option
|
||||
...fileFields.map((property) => ({
|
||||
value: property.code,
|
||||
label: `${property.label} (${property.code})`,
|
||||
@@ -50,7 +52,7 @@ const Settings: React.FC<SettingsProps> = (props) => {
|
||||
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
kintone.plugin.app.setConfig({ template }, () => {
|
||||
alert('The plug-in settings have been saved. Please update the app!');
|
||||
alert(t('on-saved'));
|
||||
window.location.href = `../../flow?app=${appId}`;
|
||||
});
|
||||
};
|
||||
@@ -65,27 +67,26 @@ const Settings: React.FC<SettingsProps> = (props) => {
|
||||
|
||||
return (
|
||||
<section className="settings">
|
||||
<KintonePluginLabel>Settings for the Kintone Word output plugin</KintonePluginLabel>
|
||||
<KintonePluginLabel>{t('title')}</KintonePluginLabel>
|
||||
<form onSubmit={handleOnSubmit}>
|
||||
<KintonePluginRow>
|
||||
<KintonePluginTitle>
|
||||
Template<KintonePluginRequire>*</KintonePluginRequire>
|
||||
{t('settings.template.title')}
|
||||
<KintonePluginRequire>*</KintonePluginRequire>
|
||||
</KintonePluginTitle>
|
||||
<KintonePluginDesc>Select a file field that contains the Word template file.</KintonePluginDesc>
|
||||
<KintonePluginDesc> {t('settings.template.description')}</KintonePluginDesc>
|
||||
{fileFields.length === 0 ? (
|
||||
<KintonePluginAlert>
|
||||
No file fields found in the app. Please add a file field to use this plugin.
|
||||
</KintonePluginAlert>
|
||||
<KintonePluginAlert>{t('settings.template.errors.no-attachment-field-found')}</KintonePluginAlert>
|
||||
) : (
|
||||
<KintonePluginSelect value={template} onChange={handleOnChangeTemplate} options={options} />
|
||||
)}
|
||||
</KintonePluginRow>
|
||||
<KintonePluginRow className={styles.buttons}>
|
||||
<KintonePluginButton variant="dialog-cancel" type="button" onClick={handleOnClickCancel}>
|
||||
Cancel
|
||||
{t('buttons.cancel')}
|
||||
</KintonePluginButton>
|
||||
<KintonePluginButton variant="dialog-ok" type="submit">
|
||||
Save
|
||||
{t('buttons.save')}
|
||||
</KintonePluginButton>
|
||||
</KintonePluginRow>
|
||||
</form>
|
||||
|
27
src/config/i18n.ts
Normal file
27
src/config/i18n.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import kintoneLanguageDetector from '../common/kintoneLanguageDetector';
|
||||
|
||||
import enTranslation from './locales/en.json';
|
||||
import jaTranslation from './locales/ja.json';
|
||||
|
||||
i18n
|
||||
.use(kintoneLanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation,
|
||||
},
|
||||
ja: {
|
||||
translation: jaTranslation,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
@@ -6,6 +6,8 @@ import ConfigApp from './ConfigApp';
|
||||
|
||||
import '../common/ui/51-modern-default.css';
|
||||
|
||||
import './i18n';
|
||||
|
||||
((PLUGIN_ID) => {
|
||||
const root = document.getElementById('plugin-config-root');
|
||||
invariant(root, 'The plugin configuration root element "plugin-config-root" is not found.');
|
||||
|
20
src/config/locales/en.json
Normal file
20
src/config/locales/en.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Settings for the Kintone Word output plugin",
|
||||
"settings": {
|
||||
"template": {
|
||||
"title": "Template",
|
||||
"description": "Select a attachment field that contains the Word template file.",
|
||||
"messages": {
|
||||
"select-an-attachment-fields": "(Select an attachment field)"
|
||||
},
|
||||
"errors": {
|
||||
"no-attachment-field-found": "No attachment fields found in the app. Please add an attachment field to use this plugin."
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"on-saved": "The plug-in settings have been saved. Please update the app!"
|
||||
}
|
20
src/config/locales/ja.json
Normal file
20
src/config/locales/ja.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Word出力プラグイン設定",
|
||||
"settings": {
|
||||
"template": {
|
||||
"title": "雛形ファイル",
|
||||
"description": "Word雛形ファイルを保存する添付ファイルフィールドを選択してください。",
|
||||
"messages": {
|
||||
"select-an-attachment-fields": "(選択してください)"
|
||||
},
|
||||
"errors": {
|
||||
"no-attachment-field-found": "このアプリに添付ファイルフィールドが見つかりません。フォームに添付ファイルフィールドを追加してください。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"save": "保存",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"on-saved": "設定を保存しました。アプリを更新してください。"
|
||||
}
|
@@ -8,9 +8,9 @@ import expressionParser from 'docxtemplater/expressions';
|
||||
import { saveAs } from 'file-saver';
|
||||
import moize from 'moize';
|
||||
import PizZip from 'pizzip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { DOCX_CONTENT_TYPE } from '../common/constants';
|
||||
import { LANGUAGE } from '../common/kintoneUtils';
|
||||
import { KintoneFormFieldProperties } from '../common/types';
|
||||
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
|
||||
import KintonePluginButton from '../common/ui/KintonePluginButton';
|
||||
@@ -26,7 +26,7 @@ interface MenuPanelProps {
|
||||
|
||||
const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise<KintoneFormFieldProperties> => {
|
||||
const client = new KintoneRestAPIClient();
|
||||
const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false });
|
||||
const { properties } = await client.app.getFormFields({ app: appId, preview: false });
|
||||
return properties;
|
||||
});
|
||||
|
||||
@@ -72,7 +72,11 @@ const formatNumberRecordValue = (
|
||||
return formatValueWithUnit(formattedValue, property);
|
||||
};
|
||||
|
||||
const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldProperty.Calc): string => {
|
||||
const formatCalculatedRecordValue = (
|
||||
value: string,
|
||||
property: KintoneFormFieldProperty.Calc,
|
||||
language: string,
|
||||
): string => {
|
||||
const { format } = property;
|
||||
if (value === '') {
|
||||
return '';
|
||||
@@ -108,7 +112,7 @@ const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldPr
|
||||
!isNaN(hours) && !isNaN(minutes),
|
||||
`Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`,
|
||||
);
|
||||
formattedValue = LANGUAGE === 'ja' ? `${hours}時間${minutes}分` : `${hours} hours ${minutes} minutes`;
|
||||
formattedValue = language === 'ja' ? `${hours}時間${minutes}分` : `${hours} hours ${minutes} minutes`;
|
||||
} else if (format === 'DAY_HOUR_MINUTE') {
|
||||
// "49:30" -> en: "2 days 1 hour 30 minutes", ja: "2日1時間30分
|
||||
const [hours, minutes] = value.split(':').map((v) => Number(v));
|
||||
@@ -119,7 +123,7 @@ const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldPr
|
||||
const days = Math.floor(Number(hours) / 24);
|
||||
const remainingHours = Number(hours) % 24;
|
||||
formattedValue =
|
||||
LANGUAGE === 'ja'
|
||||
language === 'ja'
|
||||
? `${days}日${remainingHours}時間${minutes}分`
|
||||
: `${days} days ${remainingHours} hours ${minutes} minutes`;
|
||||
} else {
|
||||
@@ -129,7 +133,11 @@ const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldPr
|
||||
return formatValueWithUnit(formattedValue, property);
|
||||
};
|
||||
|
||||
const record2data = (properties: KintoneFormFieldProperties, record: Partial<KintoneRecord>): TemplateData => {
|
||||
const record2data = (
|
||||
properties: KintoneFormFieldProperties,
|
||||
record: Partial<KintoneRecord>,
|
||||
language: string,
|
||||
): TemplateData => {
|
||||
const data: TemplateData = {};
|
||||
for (const key in record) {
|
||||
if (Object.prototype.hasOwnProperty.call(record, key) && Object.prototype.hasOwnProperty.call(properties, key)) {
|
||||
@@ -149,7 +157,7 @@ const record2data = (properties: KintoneFormFieldProperties, record: Partial<Kin
|
||||
data[key] = formatNumberRecordValue(value, property);
|
||||
} else if (type === 'CALC') {
|
||||
invariant(property.type === 'CALC', `Expected property type to be CALC, but got ${property.type}`);
|
||||
data[key] = formatCalculatedRecordValue(value, property);
|
||||
data[key] = formatCalculatedRecordValue(value, property, language);
|
||||
} else if (type === 'CHECK_BOX' || type === 'MULTI_SELECT' || type === 'CATEGORY') {
|
||||
data[key] = value.map((v) => v);
|
||||
} else if (
|
||||
@@ -163,7 +171,7 @@ const record2data = (properties: KintoneFormFieldProperties, record: Partial<Kin
|
||||
});
|
||||
} else if (type === 'SUBTABLE') {
|
||||
invariant(property.type === 'SUBTABLE', `Expected property type to be SUBTABLE, but got ${property.type}`);
|
||||
data[key] = value.map((subRecord) => record2data(property.fields, subRecord.value));
|
||||
data[key] = value.map((subRecord) => record2data(property.fields, subRecord.value, language));
|
||||
} else if (type === 'FILE') {
|
||||
invariant(property.type === 'FILE', `Expected property type to be FILE, but got ${property.type}`);
|
||||
data[key] = value.map((file) => ({
|
||||
@@ -184,31 +192,46 @@ const record2data = (properties: KintoneFormFieldProperties, record: Partial<Kin
|
||||
|
||||
const MenuPanel: React.FC<MenuPanelProps> = (props) => {
|
||||
const { pluginId, event } = props;
|
||||
const { t } = useTranslation();
|
||||
const appId = event.appId;
|
||||
const properties = React.use(cachedFormFieldsProperties(appId));
|
||||
const language = kintone.getLoginUser().language;
|
||||
const config = kintone.plugin.app.getConfig(pluginId);
|
||||
const template: string = config.template ?? '';
|
||||
|
||||
if (template === '') {
|
||||
return (
|
||||
<KintonePluginAlert>
|
||||
Word output plugin: Template field is not set. Please configure the plugin.
|
||||
{t('name')}: {t('errors.template-field-is-not-set')}
|
||||
</KintonePluginAlert>
|
||||
);
|
||||
}
|
||||
const record = event.record[template];
|
||||
if (record == null) {
|
||||
return <KintonePluginAlert>Word output plugin: Template field is not available in this app.</KintonePluginAlert>;
|
||||
return (
|
||||
<KintonePluginAlert>
|
||||
{t('name')}: {t('errors.template-field-is-not-available')}
|
||||
</KintonePluginAlert>
|
||||
);
|
||||
}
|
||||
if (record.type !== 'FILE') {
|
||||
return <KintonePluginAlert>Word output plugin: Template field must be a file field.</KintonePluginAlert>;
|
||||
return (
|
||||
<KintonePluginAlert>
|
||||
{t('name')}: {t('errors.template-field-must-be-an-attachment-field')}
|
||||
</KintonePluginAlert>
|
||||
);
|
||||
}
|
||||
if (record.value.length === 0) {
|
||||
return <KintonePluginAlert>Word output plugin: Template field does not contain any files.</KintonePluginAlert>;
|
||||
return (
|
||||
<KintonePluginAlert>
|
||||
{t('name')}: {t('errors.template-field-does-not-contain-any-files')}
|
||||
</KintonePluginAlert>
|
||||
);
|
||||
}
|
||||
if (record.value.length > 1) {
|
||||
return (
|
||||
<KintonePluginAlert>
|
||||
Word output plugin: Template field contains multiple files. Please ensure it contains only one file.
|
||||
{t('name')}: {t('errors.template-field-contains-multiple-files')}
|
||||
</KintonePluginAlert>
|
||||
);
|
||||
}
|
||||
@@ -216,7 +239,7 @@ const MenuPanel: React.FC<MenuPanelProps> = (props) => {
|
||||
if (contentType !== DOCX_CONTENT_TYPE) {
|
||||
return (
|
||||
<KintonePluginAlert>
|
||||
Word output plugin: The template file must be a DOCX file. The current file type is {contentType}.
|
||||
{t('name')}: {t('errors.template-file-must-be-a-docx', { contentType })}
|
||||
</KintonePluginAlert>
|
||||
);
|
||||
}
|
||||
@@ -234,7 +257,7 @@ const MenuPanel: React.FC<MenuPanelProps> = (props) => {
|
||||
parser: expressionParser,
|
||||
nullGetter: () => '',
|
||||
});
|
||||
doc.render(record2data(properties, event.record));
|
||||
doc.render(record2data(properties, event.record, language));
|
||||
const out = doc.getZip().generate({ type: 'blob', mimeType: DOCX_CONTENT_TYPE });
|
||||
saveAs(out, 'output.docx');
|
||||
})
|
||||
@@ -246,7 +269,7 @@ const MenuPanel: React.FC<MenuPanelProps> = (props) => {
|
||||
|
||||
return (
|
||||
<KintonePluginButton variant="normal" onClick={handleOnClickOutputButton}>
|
||||
Word出力
|
||||
{t('buttons.output')}
|
||||
</KintonePluginButton>
|
||||
);
|
||||
};
|
||||
|
27
src/desktop/i18n.ts
Normal file
27
src/desktop/i18n.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import kintoneLanguageDetector from '../common/kintoneLanguageDetector';
|
||||
|
||||
import enTranslation from './locales/en.json';
|
||||
import jaTranslation from './locales/ja.json';
|
||||
|
||||
i18n
|
||||
.use(kintoneLanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation,
|
||||
},
|
||||
ja: {
|
||||
translation: jaTranslation,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
@@ -6,6 +6,8 @@ import DesktopApp from './DesktopApp';
|
||||
|
||||
import '../common/ui/51-modern-default.css';
|
||||
|
||||
import './i18n';
|
||||
|
||||
((PLUGIN_ID) => {
|
||||
kintone.events.on(
|
||||
['app.record.detail.show', 'mobile.app.record.detail.show'],
|
||||
|
14
src/desktop/locales/en.json
Normal file
14
src/desktop/locales/en.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Word output plugin",
|
||||
"errors": {
|
||||
"template-field-is-not-set": "Template field is not set. Please configure the plugin.",
|
||||
"template-field-is-not-available": "Template field is not available in this app.",
|
||||
"template-field-must-be-an-attachment-field": "Template field must be an attachment field.",
|
||||
"template-field-does-not-contain-any-files": "Template field does not contain any files.",
|
||||
"template-field-contains-multiple-files": "Template field contains multiple files. Please ensure it contains only one file.",
|
||||
"template-file-must-be-a-docx": "The template file must be a Word (.docx) file. 'Mime-Type: {{contentType}}'."
|
||||
},
|
||||
"buttons": {
|
||||
"output": "Output Word"
|
||||
}
|
||||
}
|
14
src/desktop/locales/ja.json
Normal file
14
src/desktop/locales/ja.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Word出力プラグイン",
|
||||
"errors": {
|
||||
"template-field-is-not-set": "雛形ファイルが設定されていません。プラグインの設定を見直してください。",
|
||||
"template-field-is-not-available": "雛形ファイルに設定された添付ファイルフィールドが見つかりませんでした。プラグインの設定を見直してください。",
|
||||
"template-field-must-be-an-attachment-field": "雛形ファイルに設定されたフィールド型が添付ファイルフィールドではありません。プラグインの設定を見直してください。",
|
||||
"template-field-does-not-contain-any-files": "雛形ファイルが添付されていません。",
|
||||
"template-field-contains-multiple-files": "雛形ファイルとして複数登録されています。1つのみを指定してください。",
|
||||
"template-file-must-be-a-docx": "雛形ファイルがWord形式(.docx)ではありません。'Mime-Type: {{contentType}}'"
|
||||
},
|
||||
"buttons": {
|
||||
"output": "Word出力"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user