feat: Server side translation setup (#4657)

* Server side translation setup

* docs
This commit is contained in:
Tom Moor
2023-01-07 11:52:09 -08:00
committed by GitHub
parent a333f48102
commit 53414ec3ba
13 changed files with 185 additions and 78 deletions

View File

@@ -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`. \nYouve 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 }}. \nYouve 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 couldnt find an integration for your team. Head to your ${env.APP_NAME} settings to set one up.`,
text: t(
`Sorry, we couldnt 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 havent signed in to ${env.APP_NAME} yet, so results may be limited)`;
const haventSignedIn = t(
`It looks like you havent 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})…`,
};
}
});

View File

@@ -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) {

View File

@@ -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
View 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;
}