diff --git a/apps/app/config/i18next.config.js b/apps/app/config/i18next.config.js new file mode 100644 index 00000000000..6f7d65c306a --- /dev/null +++ b/apps/app/config/i18next.config.js @@ -0,0 +1,16 @@ +const { Lang, AllLang } = require('@growi/core'); + +/** @type {Lang} */ +const defaultLang = Lang.en_US; + +/** @type {import('i18next').InitOptions} */ +const initOptions = { + fallbackLng: defaultLang.toString(), + supportedLngs: AllLang, + defaultNS: 'translation', +}; + +module.exports = { + defaultLang, + initOptions, +}; diff --git a/apps/app/config/next-i18next.config.js b/apps/app/config/next-i18next.config.js index eef6aac4941..8ae662f7eaa 100644 --- a/apps/app/config/next-i18next.config.js +++ b/apps/app/config/next-i18next.config.js @@ -2,31 +2,41 @@ const isDev = process.env.NODE_ENV === 'development'; const path = require('path'); -const { AllLang, Lang } = require('@growi/core'); +const { AllLang } = require('@growi/core'); const { isServer } = require('@growi/core/dist/utils'); -const I18nextChainedBackend = isDev ? require('i18next-chained-backend').default : undefined; -const I18NextHttpBackend = require('i18next-http-backend').default; -const I18NextLocalStorageBackend = require('i18next-localstorage-backend').default; + +const { defaultLang } = require('./i18next.config'); const HMRPlugin = isDev ? require('i18next-hmr/plugin').HMRPlugin : undefined; +/** @type {import('next-i18next').UserConfig} */ module.exports = { - defaultLang: Lang.en_US, + ...require('./i18next.config').initOptions, + i18n: { - defaultLocale: Lang.en_US, + defaultLocale: defaultLang.toString(), locales: AllLang, }, - defaultNS: 'translation', + localePath: path.resolve('./public/static/locales'), serializeConfig: false, + // eslint-disable-next-line no-nested-ternary use: isDev ? isServer() ? [new HMRPlugin({ webpack: { server: true } })] - : [I18nextChainedBackend, new HMRPlugin({ webpack: { client: true } })] + : [ + require('i18next-chained-backend').default, + new HMRPlugin({ webpack: { client: true } }), + ] : [], backend: { - backends: isServer() ? [] : [I18NextLocalStorageBackend, I18NextHttpBackend], + backends: isServer() + ? [] + : [ + require('i18next-localstorage-backend').default, + require('i18next-http-backend').default, + ], backendOptions: [ // options for i18next-localstorage-backend { expirationTime: isDev ? 0 : 24 * 60 * 60 * 1000 }, // 1 day in production @@ -34,4 +44,5 @@ module.exports = { { loadPath: '/static/locales/{{lng}}/{{ns}}.json' }, ], }, + }; diff --git a/apps/app/package.json b/apps/app/package.json index b16c410f1b9..be4c836b479 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -125,9 +125,7 @@ "helmet": "^4.6.0", "http-errors": "^2.0.0", "i18next": "^23.10.1", - "i18next-chained-backend": "^4.6.2", - "i18next-http-backend": "^2.5.0", - "i18next-localstorage-backend": "^4.2.0", + "i18next-resources-to-backend": "^1.2.1", "is-absolute-url": "^4.0.1", "is-iso-date": "^0.0.1", "ldapjs": "^3.0.2", @@ -253,7 +251,10 @@ "fslightbox-react": "^1.7.6", "handsontable": "=6.2.2", "happy-dom": "^13.2.0", + "i18next-chained-backend": "^4.6.2", "i18next-hmr": "^3.0.4", + "i18next-http-backend": "^2.5.0", + "i18next-localstorage-backend": "^4.2.0", "jest": "^29.5.0", "jest-date-mock": "^1.0.8", "jest-localstorage-mock": "^2.4.14", diff --git a/apps/app/public/static/locales/en_US/translation.json b/apps/app/public/static/locales/en_US/translation.json index 6ee63b6bf99..8ea1b9578d7 100644 --- a/apps/app/public/static/locales/en_US/translation.json +++ b/apps/app/public/static/locales/en_US/translation.json @@ -863,5 +863,8 @@ "show_wip_page": "Show WIP", "size_s": "Size: S", "size_l": "Size: L" + }, + "create_page": { + "untitled": "Untitled" } } diff --git a/apps/app/public/static/locales/ja_JP/translation.json b/apps/app/public/static/locales/ja_JP/translation.json index 61a0c2c0c8b..47ae599eb3b 100644 --- a/apps/app/public/static/locales/ja_JP/translation.json +++ b/apps/app/public/static/locales/ja_JP/translation.json @@ -896,5 +896,8 @@ "show_wip_page": "WIP を表示", "size_s": "サイズ: S", "size_l": "サイズ: L" + }, + "create_page": { + "untitled": "無題のページ" } } diff --git a/apps/app/public/static/locales/zh_CN/translation.json b/apps/app/public/static/locales/zh_CN/translation.json index 7de4d7c54fa..57eb0ce3ff5 100644 --- a/apps/app/public/static/locales/zh_CN/translation.json +++ b/apps/app/public/static/locales/zh_CN/translation.json @@ -866,5 +866,8 @@ "show_wip_page": "显示 WIP", "size_s": "尺寸: S", "size_l": "尺寸: L" + }, + "create_page": { + "untitled": "Untitled" } } diff --git a/apps/app/src/client/util/locale-utils.ts b/apps/app/src/client/util/locale-utils.ts index f6d822c2a9f..1b741ce37d2 100644 --- a/apps/app/src/client/util/locale-utils.ts +++ b/apps/app/src/client/util/locale-utils.ts @@ -2,7 +2,7 @@ import type { IncomingHttpHeaders } from 'http'; import { Lang } from '@growi/core'; -import * as nextI18NextConfig from '^/config/next-i18next.config'; +import { defaultLang } from '^/config/i18next.config'; // https://docs.google.com/spreadsheets/d/1FoYdyEraEQuWofzbYCDPKN7EdKgS_2ZrsDrOA8scgwQ const DIAGRAMS_NET_LANG_MAP = { @@ -31,7 +31,7 @@ const getPreferredLanguage = (sortedAcceptLanguagesArray: string[]): Lang => { const matchingLang = Object.keys(ACCEPT_LANG_MAP).find(key => lang.includes(key)); if (matchingLang) return ACCEPT_LANG_MAP[matchingLang]; } - return nextI18NextConfig.defaultLang; + return defaultLang; }; /** @@ -44,7 +44,7 @@ export const detectLocaleFromBrowserAcceptLanguage = (headers: IncomingHttpHeade const acceptLanguages = headers['accept-language']; if (acceptLanguages == null) { - return nextI18NextConfig.defaultLang; + return defaultLang; } // 1. trim blank spaces. diff --git a/apps/app/src/server/routes/apiv3/page/create-page.ts b/apps/app/src/server/routes/apiv3/page/create-page.ts index d66e1363ee3..aa2b21ca75b 100644 --- a/apps/app/src/server/routes/apiv3/page/create-page.ts +++ b/apps/app/src/server/routes/apiv3/page/create-page.ts @@ -22,6 +22,7 @@ import { import type { PageDocument, PageModel } from '~/server/models/page'; import PageTagRelation from '~/server/models/page-tag-relation'; import { configManager } from '~/server/service/config-manager'; +import { getTranslation } from '~/server/service/i18next'; import loggerFactory from '~/utils/logger'; import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator'; @@ -43,8 +44,9 @@ async function generateUntitledPath(parentPath: string, basePathname: string, in } async function determinePath(_parentPath?: string, _path?: string, optionalParentPath?: string): Promise { - // TODO: https://redmine.weseek.co.jp/issues/142729 - const basePathname = 'Untitled'; + const { t } = await getTranslation(); + + const basePathname = t?.('create_page.untitled') || 'Untitled'; if (_path != null) { const path = normalizePath(_path); diff --git a/apps/app/src/server/service/i18next.ts b/apps/app/src/server/service/i18next.ts new file mode 100644 index 00000000000..9c25b571ed2 --- /dev/null +++ b/apps/app/src/server/service/i18next.ts @@ -0,0 +1,41 @@ +import type { Lang } from '@growi/core'; +import type { TFunction, i18n } from 'i18next'; +import { createInstance } from 'i18next'; +import resourcesToBackend from 'i18next-resources-to-backend'; + +import { defaultLang, initOptions } from '^/config/i18next.config'; + +import { configManager } from './config-manager'; + + +const initI18next = async(lang: Lang = defaultLang) => { + const i18nInstance = createInstance(); + await i18nInstance + .use( + resourcesToBackend( + (language: string, namespace: string) => { + return import(`^/public/static/locales/${language}/${namespace}.json`); + }, + ), + ) + .init({ + ...initOptions, + lng: lang, + }); + return i18nInstance; +}; + +type Translation = { + t: TFunction, + i18n: i18n +} + +export async function getTranslation(lang?: Lang): Promise { + const globalLang = configManager.getConfig('crowi', 'app:globalLang') as Lang; + const i18nextInstance = await initI18next(globalLang); + + return { + t: i18nextInstance.getFixedT(lang ?? globalLang), + i18n: i18nextInstance, + }; +} diff --git a/yarn.lock b/yarn.lock index c3fa76a2dac..ec11a541314 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10061,6 +10061,13 @@ i18next-localstorage-backend@^4.2.0: dependencies: "@babel/runtime" "^7.22.15" +i18next-resources-to-backend@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz#fded121e63e3139ce839c9901b9449dbbea7351d" + integrity sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw== + dependencies: + "@babel/runtime" "^7.23.2" + i18next@^23.10.1: version "23.10.1" resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.10.1.tgz#217ce93b75edbe559ac42be00a20566b53937df6"