feat: Server side translation setup (#4657)
* Server side translation setup * docs
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import crypto from "crypto";
|
||||
import { t } from "i18next";
|
||||
import Router from "koa-router";
|
||||
import { escapeRegExp } from "lodash";
|
||||
import { IntegrationService } from "@shared/types";
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
import SearchHelper from "@server/models/helpers/SearchHelper";
|
||||
import { presentSlackAttachment } from "@server/presenters";
|
||||
import { APIContext } from "@server/types";
|
||||
import { opts } from "@server/utils/i18n";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import { assertPresent } from "@server/validation";
|
||||
|
||||
@@ -150,21 +152,6 @@ router.post("hooks.slack", async (ctx: APIContext) => {
|
||||
assertPresent(user_id, "user_id is required");
|
||||
verifySlackToken(token);
|
||||
|
||||
// Handle "help" command or no input
|
||||
if (text.trim() === "help" || !text.trim()) {
|
||||
ctx.body = {
|
||||
response_type: "ephemeral",
|
||||
text: "How to use /outline",
|
||||
attachments: [
|
||||
{
|
||||
text:
|
||||
"To search your knowledge base use `/outline keyword`. \nYou’ve already learned how to get help with `/outline help`.",
|
||||
},
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
let user, team;
|
||||
// attempt to find the corresponding team for this request based on the team_id
|
||||
team = await Team.findOne({
|
||||
@@ -225,12 +212,39 @@ router.post("hooks.slack", async (ctx: APIContext) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle "help" command or no input
|
||||
if (text.trim() === "help" || !text.trim()) {
|
||||
ctx.body = {
|
||||
response_type: "ephemeral",
|
||||
text: "How to use /outline",
|
||||
attachments: [
|
||||
{
|
||||
text: t(
|
||||
"To search your knowledgebase use {{ command }}. \nYou’ve already learned how to get help with {{ command2 }}.",
|
||||
{
|
||||
command: `/outline keyword`,
|
||||
command2: `/outline help`,
|
||||
...opts(user),
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// This should be super rare, how does someone end up being able to make a valid
|
||||
// request from Slack that connects to no teams in Outline.
|
||||
if (!team) {
|
||||
ctx.body = {
|
||||
response_type: "ephemeral",
|
||||
text: `Sorry, we couldn’t find an integration for your team. Head to your ${env.APP_NAME} settings to set one up.`,
|
||||
text: t(
|
||||
`Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.`,
|
||||
{
|
||||
...opts(user),
|
||||
appName: env.APP_NAME,
|
||||
}
|
||||
),
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -292,7 +306,13 @@ router.post("hooks.slack", async (ctx: APIContext) => {
|
||||
query: text,
|
||||
results: totalCount,
|
||||
});
|
||||
const haventSignedIn = `(It looks like you haven’t signed in to ${env.APP_NAME} yet, so results may be limited)`;
|
||||
const haventSignedIn = t(
|
||||
`It looks like you haven’t signed in to {{ appName }} yet, so results may be limited`,
|
||||
{
|
||||
...opts(user),
|
||||
appName: env.APP_NAME,
|
||||
}
|
||||
);
|
||||
|
||||
// Map search results to the format expected by the Slack API
|
||||
if (results.length) {
|
||||
@@ -312,7 +332,7 @@ router.post("hooks.slack", async (ctx: APIContext) => {
|
||||
? [
|
||||
{
|
||||
name: "post",
|
||||
text: "Post to Channel",
|
||||
text: t("Post to Channel", opts(user)),
|
||||
type: "button",
|
||||
value: result.document.id,
|
||||
},
|
||||
@@ -324,15 +344,24 @@ router.post("hooks.slack", async (ctx: APIContext) => {
|
||||
|
||||
ctx.body = {
|
||||
text: user
|
||||
? `This is what we found for "${text}"…`
|
||||
: `This is what we found for "${text}" ${haventSignedIn}…`,
|
||||
? t(`This is what we found for "{{ term }}"`, {
|
||||
...opts(user),
|
||||
term: text,
|
||||
})
|
||||
: t(`This is what we found for "{{ term }}"`, {
|
||||
term: text,
|
||||
}) + ` (${haventSignedIn})…`,
|
||||
attachments,
|
||||
};
|
||||
} else {
|
||||
ctx.body = {
|
||||
text: user
|
||||
? `No results for "${text}"`
|
||||
: `No results for "${text}" ${haventSignedIn}`,
|
||||
? t(`No results for "{{ term }}"`, {
|
||||
...opts(user),
|
||||
term: text,
|
||||
})
|
||||
: t(`No results for "{{ term }}"`, { term: text }) +
|
||||
` (${haventSignedIn})…`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import mount from "koa-mount";
|
||||
import enforceHttps, { xForwardedProtoResolver } from "koa-sslify";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { initI18n } from "@server/utils/i18n";
|
||||
import routes from "../routes";
|
||||
import api from "../routes/api";
|
||||
import auth from "../routes/auth";
|
||||
@@ -37,6 +38,8 @@ if (env.CDN_URL) {
|
||||
}
|
||||
|
||||
export default function init(app: Koa = new Koa()): Koa {
|
||||
initI18n();
|
||||
|
||||
if (isProduction) {
|
||||
// Force redirect to HTTPS protocol unless explicitly disabled
|
||||
if (env.FORCE_HTTPS) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { setResource } from "@server/logging/tracer";
|
||||
import { traceFunction } from "@server/logging/tracing";
|
||||
import { initI18n } from "@server/utils/i18n";
|
||||
import {
|
||||
globalEventQueue,
|
||||
processorEventQueue,
|
||||
@@ -11,6 +12,8 @@ import processors from "../queues/processors";
|
||||
import tasks from "../queues/tasks";
|
||||
|
||||
export default function init() {
|
||||
initI18n();
|
||||
|
||||
// This queue processes the global event bus
|
||||
globalEventQueue.process(
|
||||
traceFunction({
|
||||
|
||||
58
server/utils/i18n.ts
Normal file
58
server/utils/i18n.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import path from "path";
|
||||
import i18n from "i18next";
|
||||
import backend from "i18next-fs-backend";
|
||||
import { languages } from "@shared/i18n";
|
||||
import { unicodeBCP47toCLDR, unicodeCLDRtoBCP47 } from "@shared/utils/date";
|
||||
import env from "@server/env";
|
||||
import { User } from "@server/models";
|
||||
|
||||
/**
|
||||
* Returns i18n options for the given user or the default server language if
|
||||
* no user is provided.
|
||||
*
|
||||
* @param user The user to get options for
|
||||
* @returns i18n options
|
||||
*/
|
||||
export function opts(user?: User | null) {
|
||||
return {
|
||||
lng: unicodeCLDRtoBCP47(user?.language ?? env.DEFAULT_LANGUAGE),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes i18n library, loading all available translations from the
|
||||
* filesystem.
|
||||
*
|
||||
* @returns i18n instance
|
||||
*/
|
||||
export function initI18n() {
|
||||
const lng = unicodeCLDRtoBCP47(env.DEFAULT_LANGUAGE);
|
||||
i18n.use(backend).init({
|
||||
compatibilityJSON: "v3",
|
||||
backend: {
|
||||
loadPath: (language: string) => {
|
||||
return path.resolve(
|
||||
path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"shared",
|
||||
"i18n",
|
||||
"locales",
|
||||
unicodeBCP47toCLDR(language),
|
||||
"translation.json"
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
preload: languages.map(unicodeCLDRtoBCP47),
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
lng,
|
||||
fallbackLng: lng,
|
||||
keySeparator: false,
|
||||
returnNull: false,
|
||||
});
|
||||
return i18n;
|
||||
}
|
||||
Reference in New Issue
Block a user