Compare commits

...

79 Commits

Author SHA1 Message Date
Evgeny e026250893 1.8.1 (#34)
* bump version to 1.8.1

* update translation
2026-06-10 06:55:14 +05:00
Evgeny d2be66831d 1.8.0 (#33)
* bump version to 1.8.0

* update Dockerfile

* update translation
2026-06-02 04:59:47 +05:00
flameshikari 49472ac801 update translations
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2026-05-04 10:33:19 +05:00
flameshikari a81bd3bd17 bump version to 1.7.1 2026-05-04 10:22:47 +05:00
flameshikari f22d5952bb fix workflow permission
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2026-05-01 09:31:35 +05:00
flameshikari 02bdd461b4 fix workflow token 2026-05-01 09:28:49 +05:00
Evgeny d0abf84aa8 1.7.0 (#32)
* bump version to 1.7.0

* update HMR mode; make a single dev container for every service; etc

* update translations
2026-05-01 09:14:01 +05:00
Aleksandr Posazhennikov b3bda3622c upd: ru.json (#31)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* upd: ru.json

Заменил переводы некоторых фраз на более подходящие по смыслу и синтаксису

* return indents in ru.json

---------

Co-authored-by: flameshikari <mail@hexed.pw>
2026-04-14 00:05:10 +05:00
flameshikari 856cf4b0c9 update translations
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2026-03-21 00:11:08 +05:00
flameshikari 0bc1c14a9c bump version to 1.6.1 2026-03-21 00:10:47 +05:00
Evgeny 2c681f14f2 1.5.0 (#30)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump version to 1.5.0

* update translation.json
2026-02-16 15:13:27 +05:00
Evgeny 2914b54933 1.4.0 (#29)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump version to 1.4.0

* update .dockerignore

* update translation.json
2026-01-28 16:09:50 +05:00
Evgeny 076879fd63 1.3.0 (#28)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump version to 1.3.0

* update translation.json

* update the workflow
2026-01-21 15:49:17 +05:00
Evgeny fbe5d1adb1 1.2.0 (#27)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump version to 1.2.0

* update translation.json

* update workflows

* update dockerfiles
2026-01-08 22:30:32 +05:00
flameshikari b7c7d5a0ac bump GH Actions versions
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-11-17 18:26:44 +05:00
flameshikari 2fe50b5784 update translations 2025-11-17 18:22:42 +05:00
flameshikari 08aef9b911 bump version to 1.1.0 2025-11-17 18:20:59 +05:00
flameshikari d963ac76bf update Dockerfile*
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-10-29 19:21:40 +05:00
flameshikari afedb4b0ca update translations 2025-10-29 19:21:23 +05:00
flameshikari 2ccc82c02e bump version to 1.0.1 2025-10-29 19:21:05 +05:00
Evgeny daa7561aff 1.0.0 (#26)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump outline to 1.0.0-2

* update translation.json

* update readme.*

* update Dockerfile*

* bump outline to 1.0.0-test8

* reorganize repo, add git hook

* bump version to 1.0.0

* update translations
2025-10-27 17:47:11 +05:00
flameshikari 7bcf8279e2 update translation.json
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-09-19 11:06:49 +05:00
flameshikari bc93c415d8 bump outline to v0.87.4 2025-09-19 10:39:14 +05:00
flameshikari 7cbc22e1bf update translation.json
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-09-02 01:54:18 +05:00
flameshikari 73dd19e49b update readme and move to .github dir 2025-09-02 01:53:40 +05:00
flameshikari b9bfdc024f bump outline to v0.87.3 2025-09-02 01:46:57 +05:00
flameshikari 56c8631c5a update readme
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-09-01 01:34:31 +05:00
flameshikari 74c29e1934 add the script for bumping versions in readme 2025-09-01 01:31:09 +05:00
flameshikari d3399ba110 update translation.json 2025-09-01 00:50:15 +05:00
flameshikari c6e5a87421 update vite.patch 2025-09-01 00:50:01 +05:00
flameshikari 0675c06f23 update diff.py 2025-09-01 00:49:54 +05:00
flameshikari ddb2decad0 bump outline to v0.87.0 2025-08-31 23:13:48 +05:00
flameshikari 3a3728b9bd bump outline to v0.86.1 2025-08-11 12:23:09 +05:00
flameshikari be856bfbdc update translations
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-08-06 22:40:05 +05:00
flameshikari 82256f4ada bump outline to v0.86.0 2025-08-06 20:38:08 +05:00
flameshikari c7d513f03e bump outline to v0.85.1 2025-07-13 02:19:06 +05:00
sovushik 8d9d6c7f14 Обновление перевода (#25)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* Update translation.json

Скорректировал перевод

* Update translation.json

Еще парочка исправлений
2025-07-06 18:53:13 +05:00
flameshikari c794997b26 update readme
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-07-05 08:20:37 +05:00
Evgeny 4485a10514 hot reload (#24)
* container with hot reload

* update readme
2025-07-05 08:18:15 +05:00
flameshikari b129a74d39 update readme 2025-07-04 23:58:27 +05:00
Evgeny d22c3ab04c 0.85.0 (#23)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump outline to v0.85.0

* bump Node version in Dockerfile

* use new OIDC_ISSUER_URL in docker-compose.yml (v0.85.0)

* update translations

* update the workflow

* update readme
2025-07-04 14:14:52 +05:00
Evgeny cf2d26b011 0.84.0 (#19)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* update diff.py for handling ru plurals

* update translations

* add -m option to create home folder for npm cache

* bump outline to v0.84.0

* update translations
2025-05-12 17:26:59 +05:00
flameshikari 2c34b016fe fix Dockerfile order
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-04-12 03:35:18 +05:00
flameshikari 16ddf6f2b6 some fixes 2025-04-12 01:07:12 +05:00
Evgeny 032263ec5d 0.83.0 (#17)
* add patch for version checking of outline-ru, not the original one

* optimize Dockerfile and add docker-compose.yml for deploying on localhost for testing purposes

* make ru_RU as the default language, combine patches into one

* update the workflow

* update docker* files

* rename the script

* testing translations

* add concurrency and fix regexps in the workflow

* disable action output

* bump outline to 0.83.0

* update Dockerfile

* update translations

* update readme

* update readme

* clear trailing whitespaces
2025-04-12 00:46:50 +05:00
Evgeny e098d1a7df 0.82.0
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
refactoring and fixes
2025-03-11 22:05:24 +05:00
flameshikari 5204b57626 add patch, add nightly build, reorganize the repo
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-03-11 20:21:32 +05:00
flameshikari 2751858e48 update readme 2025-03-11 17:31:01 +05:00
flameshikari bef3b69a5e update license 2025-03-11 16:12:01 +05:00
flameshikari af60692963 remove shared dir 2025-03-11 15:39:23 +05:00
flameshikari fdb2cc6bc5 change submodule name 2025-03-11 15:36:15 +05:00
flameshikari 0070936e30 fix HEALTHCHECK
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-02-27 15:15:32 +05:00
Evgeny dcc106c3d4 0.82.0
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
0.82.0
2025-02-16 10:59:57 +05:00
flameshikari c95ebc679d update translations
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-02-16 10:46:17 +05:00
flameshikari cf36d91024 bump src to 0.82.0 2025-02-16 09:46:17 +05:00
flameshikari 39a62bc345 update the workflow triggers
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-02-01 19:31:52 +05:00
flameshikari 55e73bc81c update translations 2025-02-01 19:29:14 +05:00
flameshikari 735a276f68 bump LICENSE 2025-02-01 19:06:55 +05:00
flameshikari f97ff78deb update for the workflow for faster arm64 building process 2025-02-01 19:03:59 +05:00
flameshikari f88eb75c6e bump src to 0.82.0 2025-02-01 16:35:26 +05:00
flameshikari 979766198a bump src to 0.81.1
Build / Build (push) Has been cancelled
2024-11-20 19:46:57 +05:00
Evgeny 329bbee347 0.81.0
Build / Build (push) Has been cancelled
0.81.0
2024-11-11 20:14:56 +05:00
flameshikari b9e467a923 update shared files 2024-11-11 20:09:21 +05:00
flameshikari 2a40731b78 update script 2024-11-11 20:09:10 +05:00
flameshikari ca2ae2666a update translations 2024-11-11 20:07:39 +05:00
flameshikari a85bc68a35 bump src to 0.81.0 2024-11-11 19:42:15 +05:00
flameshikari a514aba673 bump src to 0.80.2 2024-09-26 18:27:08 +05:00
flameshikari 7301597cde add outline license and update readme 2024-09-23 02:11:00 +05:00
Evgeny d7507c0d96 0.80.1
0.80.1
2024-09-22 22:08:09 +05:00
flameshikari 231e8598ed bump src to v0.80.1 2024-09-22 22:05:54 +05:00
Evgeny 6f30a76e57 0.80.0
0.80.0
2024-09-22 22:02:42 +05:00
flameshikari 20b6a01964 update translations 2024-09-22 20:17:26 +05:00
flameshikari 0484e8440e bump src to v0.80.0 2024-09-22 20:06:58 +05:00
Evgeny c45bba5ed8 0.79.1
0.79.1
2024-09-06 13:42:46 +05:00
flameshikari 3d175e429a update README.md 2024-09-06 13:31:16 +05:00
flameshikari d3af59b4e0 update workflows 2024-09-06 13:06:40 +05:00
flameshikari 3c80879e5e update README.md 2024-09-06 13:06:33 +05:00
flameshikari dba3ed7731 bump src to 0.79.1 2024-09-06 13:06:16 +05:00
Evgeny 829e11612e update README.md 2024-09-05 15:07:59 +05:00
29 changed files with 2782 additions and 1524 deletions
+16
View File
@@ -1,2 +1,18 @@
__mocks__
.git
.vscode
.github
.circleci
.DS_Store
.env*
.eslint*
.oxlintrc*
.log
Makefile
Procfile
app.json
crowdin.yml
build
docker-compose.yml
node_modules
.yarn
+6
View File
@@ -0,0 +1,6 @@
COMPOSE_PROFILES=dev
# PORT=10240
# PORT_OIDC=10241
# PORT_VITE=10242
# SECRET=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef # openssl rand -hex 32
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
WORKDIR="$(dirname "$(readlink -f "${0}")")"
if ! git diff --cached --quiet --submodule=diff -- ./outline; then
python "$WORKDIR/update_readme.py"
git add ./README.md
exit 0
else
exit 0
fi
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python
import os
import re
import json
ignore = '0.71.0'
def resolve(path):
basepath = os.path.abspath(os.path.dirname(__file__))
abspath = os.path.abspath(basepath + '/' + path)
return abspath
def get_version(path):
with open(path, 'r') as f:
data = json.load(f)
return data.get('version', None)
def replace_version(path, version):
with open(path, 'r') as file:
content = file.read()
pattern = re.compile(r'(\d+\.\d+\.\d+(-\w+)?)')
check = lambda x: ignore if ignore == x.group(0) else version
with open(path, 'w') as file:
file.write(pattern.sub(check, content))
version = get_version(resolve('../../outline/package.json'))
replace_version(resolve('../../README.md'), version)
+122 -42
View File
@@ -4,77 +4,157 @@ on:
push:
branches:
- master
- dev
paths:
- .github
- shared
- src
- .github/workflows/**
- outline/**
- translation/ru.json
- Dockerfile
workflow_dispatch:
concurrency:
group: outline
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
DOCKER_BUILD_CHECKS_ANNOTATIONS: false
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
jobs:
build:
name: Build
runs-on: ubuntu-latest
name: Build [${{ matrix.arch }}]
runs-on: ${{ contains(matrix.arch, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
outputs:
version: ${{ steps.version.outputs.version }}
permissions:
contents: read
packages: write
strategy:
matrix:
arch:
- amd64
- arm64
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Export variable
run: |
echo "VERSION=$(cat src/package.json | jq -r '.version')" >> $GITHUB_ENV
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to the GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract Metadata
id: meta
uses: docker/metadata-action@v5
- name: Login to GitHub CR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set Version
id: version
run: |
version=$(jq -r '.version' outline/package.json)
[[ -z $version ]] && exit 1
echo "version=$version" | tee -a $GITHUB_OUTPUT
- name: Set Metadata
uses: docker/metadata-action@v6
id: metadata
with:
images: |
${{ env.IMAGE_NAME }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=raw,value=${{ env.VERSION }}
${{ github.repository }}
ghcr.io/${{ github.repository }}
- name: Build Image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v7
id: build
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags:
${{ steps.meta.outputs.tags }}
cache-from: type=gha,scope=build-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }}
platforms: linux/${{ matrix.arch }}
build-args: |
APP_PATH=/opt/outline
SRC_PATH=./outline
labels: ${{ steps.metadata.outputs.labels }}
outputs: type=image,"name=${{ github.repository }},ghcr.io/${{ github.repository }}",push-by-digest=true,name-canonical=true,push=true
- name: Export Digests
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload Digests
uses: actions/upload-artifact@v7
with:
name: digests-linux-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
publish:
name: Publish
runs-on: ubuntu-24.04
needs: build
permissions:
contents: write
packages: write
env:
version: ${{ needs.build.outputs.version }}
steps:
- name: Download Digests
uses: actions/download-artifact@v8
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub CR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Set Metadata
uses: docker/metadata-action@v6
with:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=nightly,enable=${{ github.ref == 'refs/heads/dev' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=${{ env.version }},enable=${{ github.ref == 'refs/heads/master' }}
- name: Create Manifest & Push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ github.repository }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
- name: Create Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: ${{ github.ref == 'refs/heads/master' }}
with:
name: ${{ env.VERSION }}
tag_name: ${{ env.VERSION }}
body: "[Изменения в ${{ env.VERSION }}](https://github.com/outline/outline/releases/tag/v${{ env.VERSION }})"
token: ${{ secrets.TOKEN }}
name: ${{ env.version }}
tag_name: ${{ env.version }}
body: "[Изменения в ${{ env.version }}](https://github.com/outline/outline/releases/tag/v${{ env.version }})"
token: ${{ secrets.GITHUB_TOKEN }}
-33
View File
@@ -1,33 +0,0 @@
name: Dry Build
on:
push:
branches:
- dev
paths:
- .github
- shared
- src
- Dockerfile
jobs:
build:
name: Dry Build
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
+2 -1
View File
@@ -1 +1,2 @@
scripts/translation.json
translation/*.json
!translation/ru.json
+3 -3
View File
@@ -1,3 +1,3 @@
[submodule "src"]
path = src
url = git@github.com:outline/outline.git
[submodule "outline"]
path = outline
url = https://github.com/outline/outline.git
+41 -41
View File
@@ -1,46 +1,46 @@
ARG APP_PATH=/opt/outline
ARG SRC_PATH=./outline
FROM node:20-slim AS base
FROM node:24.16.0 AS build
ARG CDN_URL
ARG APP_PATH
ARG SRC_PATH
WORKDIR $APP_PATH
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ${SRC_PATH}/.yarnrc.yml ./
COPY ${SRC_PATH}/patches ./patches
ENV NODE_OPTIONS='--max-old-space-size=24000'
RUN corepack enable && \
yarn install --immutable --network-timeout 1000000 && \
yarn cache clean
COPY ${SRC_PATH} .
COPY ./patches/lang.patch .
RUN patch -p1 < lang.patch
COPY ./translation/ru.json ./shared/i18n/locales/ru_RU/translation.json
RUN yarn build && \
yarn workspaces focus --production
FROM node:24.16.0-slim AS release
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
ENV DATA_PATH=/var/lib/outline/data
ENV USER=nodejs
RUN addgroup --gid 1001 ${USER} && \
adduser --uid 1001 --ingroup ${USER} ${USER} && \
mkdir -p ${DATA_PATH} && \
chown -R ${USER}:${USER} ${DATA_PATH}/..
ARG APP_PATH
WORKDIR $APP_PATH
FROM base AS build
COPY ./src/package.json ./src/yarn.lock ./
COPY ./src/patches ./patches
RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean
COPY src .
COPY shared ./shared
ARG CDN_URL
RUN yarn build
RUN rm -rf node_modules
RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean
ENV PORT=3000
FROM base AS release
COPY --chown=${USER}:${USER} --from=build $APP_PATH/node_modules ./node_modules
COPY --chown=${USER}:${USER} --from=build $APP_PATH/build ./build
COPY --chown=${USER}:${USER} --from=build $APP_PATH/server ./server
COPY --chown=${USER}:${USER} --from=build $APP_PATH/public ./public
COPY --chown=${USER}:${USER} --from=build $APP_PATH/.sequelizerc .
COPY --chown=${USER}:${USER} --from=build $APP_PATH/package.json .
ENV NODE_ENV=production
COPY --from=build $APP_PATH/build ./build
COPY --from=build $APP_PATH/server ./server
COPY --from=build $APP_PATH/public ./public
COPY --from=build $APP_PATH/.sequelizerc ./.sequelizerc
COPY --from=build $APP_PATH/node_modules ./node_modules
COPY --from=build $APP_PATH/package.json ./package.json
RUN apt-get update && \
apt-get install -y wget && \
rm -rf /var/lib/apt/lists/*
RUN addgroup --gid 1001 nodejs && \
adduser --uid 1001 --ingroup nodejs nodejs && \
chown -R nodejs:nodejs $APP_PATH/build && \
mkdir -p /var/lib/outline && \
chown -R nodejs:nodejs /var/lib/outline
ENV FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
RUN mkdir -p "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chown -R nodejs:nodejs "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chmod 1777 "$FILE_STORAGE_LOCAL_ROOT_DIR"
VOLUME /var/lib/outline/data
USER nodejs
HEALTHCHECK CMD wget -qO- http://localhost:${PORT}/_health | grep -q "OK" || exit 1
EXPOSE 3000
CMD ["yarn", "start"]
ENV PORT=3000
USER ${USER}
EXPOSE ${PORT}
VOLUME ${DATA_PATH}
HEALTHCHECK --interval=1m CMD curl -fs localhost:${PORT}/_health | grep -q OK || exit 1
CMD ["node", "build/server/index.js"]
+24
View File
@@ -0,0 +1,24 @@
ARG APP_PATH=/opt/outline
ARG SRC_PATH=./outline
FROM node:24.15.0
ARG APP_PATH
ARG SRC_PATH
ARG CDN_URL
WORKDIR $APP_PATH
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ${SRC_PATH}/.yarnrc.yml ./
COPY ${SRC_PATH}/patches ./patches
ENV NODE_OPTIONS='--max-old-space-size=24000'
RUN corepack enable && \
yarn install --immutable --network-timeout 1000000 && \
yarn cache clean
COPY ${SRC_PATH} .
COPY ./patches/* .
RUN for patch in $(ls *.patch); do patch -p1 < $patch; done
RUN cat << EOF > /entrypoint.sh
yarn dev:watch
EOF
ENV DATA_PATH=/var/lib/outline/data
VOLUME ${DATA_PATH}
STOPSIGNAL SIGKILL
ENTRYPOINT ["bash", "/entrypoint.sh"]
+25
View File
@@ -0,0 +1,25 @@
MIT License
Copyright (c) 2025 flameshikari
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
This repository includes Outline Wiki as a submodule, which is licensed under the Business Source License 1.1 (BSL). See the outline directory for its specific license terms.
+90 -13
View File
@@ -1,27 +1,30 @@
# 📖 [Outline](https://github.com/outline/outline) с русским переводом ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/flameshikari/outline-ru/build.yml) ![asd](https://img.shields.io/github/v/release/flameshikari/outline-ru)
# 📚 [Outline](https://github.com/outline/outline) с русским переводом [![Build Status](https://img.shields.io/github/actions/workflow/status/flameshikari/outline-ru/build.yml)](https://github.com/flameshikari/outline-ru/actions) [![Version](https://img.shields.io/github/v/release/flameshikari/outline-ru?style=)](https://github.com/flameshikari/outline-ru/releases/latest)
## Зачем?
## Зачем
Поддержка русского языка в [Outline](https://github.com/outline/outline) прекращена в версии [0.71.0](https://github.com/outline/outline/releases/tag/v0.71.0) по [некоторым причинам](https://github.com/outline/outline/discussions/5706), поэтому и существует этот репозиторий с русским переводом.
Поддержка русского языка в [Outline](https://github.com/outline/outline) прекращена в версии [0.71.0](https://github.com/outline/outline/releases/tag/v0.71.0) по [некоторым причинам](https://github.com/outline/outline/discussions/5706).
## Примечания
## 📝 Примечания
За основу взят перевод из [данного коммита](https://github.com/outline/outline/commit/228d1faa9fd3cbb82409d98e1443fed65adc5715), который впоследствии улучшается и переводится здесь. Буду рад помощи в улучшении перевода или сборки; сообщить о некорректном переводе можно [здесь](https://github.com/flameshikari/outline-ru/discussions/8).
- образ доступен в [Docker Hub](https://hub.docker.com/r/flameshikari/outline-ru/tags) и [GHCR](https://github.com/flameshikari/outline-ru/pkgs/container/outline-ru)
- за основу взят перевод из [этого коммита](https://github.com/outline/outline/commit/228d1faa9fd3cbb82409d98e1443fed65adc5715)
- сообщить о некорректном переводе можно [тут](https://github.com/flameshikari/outline-ru/discussions/8)
Из доступных архитектур контейнера имеются только `amd64` и `arm64`, потому что мультиплатформерная сборка на GitHub Actions с помощью QEMU очень медленная: остальные архитектуры собираются часами либо вовсе зависают намертво.
## 🐳 Установка
## Установка
> [!WARNING]
> Перед обновлением **ОБЯЗАТЕЛЬНО** делайте [бэкап](https://docs.getoutline.com/s/hosting/doc/backups-KZtPOADCHG)!
Всё делается по [официальной инструкции](https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t), только в качестве `image` нужно использовать `flameshikari/outline-ru:latest` или `ghcr.io/flameshikari/outline-ru:latest` (вместо `latest` можно указать версию; доступные смотреть [здесь](https://github.com/flameshikari/outline-ru/releases)).
Следуйте [официальной инструкции](https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t), только в качестве `image` укажите `flameshikari/outline-ru:latest` (желательно зафиксировать версию, заменив `latest` на один из [доступных тегов](https://github.com/flameshikari/outline-ru/tags)). Например:
```yaml
...
services:
outline:
image: flameshikari/outline-ru:latest
image: flameshikari/outline-ru:1.8.1
# image: ghcr.io/flameshikari/outline-ru:1.8.1
env_file: ./docker.env
ports:
- "3000:3000"
expose:
- 3000
volumes:
- storage-data:/var/lib/outline/data
depends_on:
@@ -30,3 +33,77 @@
...
```
## 🛠️ Разработка
### Ключевые файлы
- русский перевод — [translation/ru.json](./translation/ru.json)
- английский перевод — [outline/shared/i18n/locales/en_US/translation.json](https://github.com/outline/outline/blob/main/shared/i18n/locales/en_US/translation.json)
- временный файл — [translation/tmp.json]() (существует только локально)
### Описание работы скрипта
Скрипт [translation/merge.py](./translation/merge.py) используется для объединения английского и русского переводов во временный файл. Скрипт не имеет интерактивного режима и каких-либо аргументов/опций, он просто запускается (с выводом некоторой полезной информации) и делает следующее:
- сохраняет актуальные переведённые строки
- удаляет неактуальные переведённые строки
- если в русском переводе есть одинаковые key/value пары, то они считаются исключениями (например, `HTML` или `API`) и переносятся как есть
- новые непереведённые строки добавляются в конец
> Также скрипт конвертирует строки с суффиксом `_plural` (англ. множественное число) в строки с числовыми суффиксами, поддерживающие склонение по падежам. Например, имеем исходные данные:
>
> ```jsonc
> {
> // ...
> "{{ count }} comment": "{{ count }} comment",
> "{{ count }} comment_plural": "{{ count }} comments"
> // ...
> }
> ```
> Строки после конвертации скриптом:
> ```jsonc
> {
> // ...
> "{{ count }} comment_0": "[NOT TRANSLATED]",
> "{{ count }} comment_1": "[NOT TRANSLATED]",
> "{{ count }} comment_2": "[NOT TRANSLATED]"
> // ...
> }
> ```
> Конвертированные строки после перевода:
> ```jsonc
> {
> // ...
> "{{ count }} comment_0": "{{ count }} комментарий", // ед. число, им. падеж
> "{{ count }} comment_1": "{{ count }} комментария", // ед. число, род. падеж
> "{{ count }} comment_2": "{{ count }} комментариев" // мн. число, род. падеж
> // ...
> }
> ```
> Документация: [множественное число](https://www.i18next.com/translation-function/plurals#languages-with-multiple-plurals) в [i18next](https://www.i18next.com) с [JSON-форматом версии 3](https://www.i18next.com/misc/json-format#i18next-json-v3)
### Быстрый старт
1. Клонирование репозитория с подмодулем:
```sh
git clone --recurse-submodules git@github.com:flameshikari/outline-ru.git
```
2. Пулл изменений в подмодуле и переключение на последний доступный тег:
```sh
git submodule foreach 'git pull --rebase --tags && git checkout v1.8.1'
```
3. Запуск контейнеров:
```sh
docker compose up -d --build
```
Веб-интерфейс Outline будет доступен по [этой ссылке](http://localhost:10240); входить с помощью OpenID Connect под логином/паролем `outline`.
4. Формирование временного файла с помощью [translation/merge.py](./translation/merge.py):
```sh
python translation/merge.py
```
После можно приступить к переводу сфомированного временного файла. После перевода временного файла скопируйте его в файл русского перевода. Любые изменения в русском переводе обновят [открытую веб-страницу](http://localhost:10240) через пару секунд.
+73
View File
@@ -0,0 +1,73 @@
volumes:
outline:
name: outline
outline-postgres:
name: outline-postgres
networks:
default:
name: outline
x-outline-base: &outline-base
container_name: outline
depends_on:
- outline-services
environment:
FILE_STORAGE: local
FORCE_HTTPS: false
PORT: ${PORT:-10240}
PORT_VITE: ${PORT_VITE:-10242}
URL: http://127.0.0.1:${PORT:-10240}
SECRET_KEY: ${SECRET:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}
UTILS_SECRET: ${SECRET:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}
REDIS_URL: redis://outline-services
DATABASE_URL: postgres://outline:outline@outline-services/outline
PGSSLMODE: disable
OIDC_ISSUER_URL: http://outline-services:8080
OIDC_CLIENT_ID: outline
OIDC_CLIENT_SECRET: outline
OIDC_DISPLAY_NAME: OIDC
services:
outline:
<<: *outline-base
profiles: [prod]
build: .
image: flameshikari/outline-ru:nightly
ports:
- ${PORT:-10240}:${PORT:-10240}
pull_policy: always
volumes:
- outline:/var/lib/outline/data
outline-dev:
<<: *outline-base
build:
dockerfile: Dockerfile.dev
profiles: [dev]
ports:
- ${PORT:-10240}:${PORT:-10240}
- ${PORT_VITE:-10242}:${PORT_VITE:-10242}
volumes:
- outline:/var/lib/outline/data
- ./translation/ru.json:/opt/outline/shared/i18n/locales/ru_RU/translation.json
outline-services:
container_name: outline-services
image: outline-services
build: services
environment:
ISSUER: http://outline-services:8080
PUBLIC_URL: http://127.0.0.1:${PORT_OIDC:-10241}
volumes:
- outline-postgres:/var/lib/postgresql/data
ports:
- ${PORT_OIDC:-10241}:8080
healthcheck:
test: |
pg_isready -U outline && \
redis-cli ping && \
wget -qO- http://127.0.0.1:8080/health
interval: 10s
timeout: 5s
retries: 5
Submodule
+1
Submodule outline added at c2edd41e87
+122
View File
@@ -0,0 +1,122 @@
diff --git a/app/utils/i18n.ts b/app/utils/i18n.ts
index d20fe802a..de4987a99 100644
--- a/app/utils/i18n.ts
+++ b/app/utils/i18n.ts
@@ -51,5 +51,16 @@ export function initI18n(defaultLanguage = "en_US") {
Logger.error("Failed to initialize i18n", err);
});
+ // HMR: when a translation JSON changes on disk, the Vite dev server emits
+ // an "i18n:update" event (see vite.config.ts). Reload the resources for
+ // that language and ask react-i18next to re-render the tree.
+ if (typeof import.meta.hot !== "undefined") {
+ import.meta.hot.on("i18n:update", async ({ lng: changed }) => {
+ const target = unicodeCLDRtoBCP47(changed);
+ await i18n.reloadResources(target);
+ i18n.emit("languageChanged", i18n.language);
+ });
+ }
+
return i18n;
}
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts
index 477793da6..93bc7c6e9 100644
--- a/server/middlewares/csp.ts
+++ b/server/middlewares/csp.ts
@@ -59,8 +59,9 @@ export default function createCSPMiddleware(options?: CSPOptions) {
// Allow to load assets from Vite
if (!env.isProduction) {
- scriptSrc.push(env.URL.replace(`:${env.PORT}`, ":3001"));
- scriptSrc.push("localhost:3001");
+ const vitePort = Number(process.env.PORT_VITE) || 3001;
+ scriptSrc.push(env.URL.replace(`:${env.PORT}`, `:${vitePort}`));
+ scriptSrc.push(`localhost:${vitePort}`);
} else {
scriptSrc.push(env.URL);
}
diff --git a/server/routes/app.ts b/server/routes/app.ts
index b598752f2..11667c560 100644
--- a/server/routes/app.ts
+++ b/server/routes/app.ts
@@ -22,7 +22,8 @@ import { loadPublicShare } from "@server/commands/shareLoader";
const readFile = util.promisify(fs.readFile);
const entry = "app/index.tsx";
-const viteHost = env.URL.replace(`:${env.PORT}`, ":3001");
+const vitePort = Number(process.env.PORT_VITE) || 3001;
+const viteHost = env.URL.replace(`:${env.PORT}`, `:${vitePort}`);
let indexHtmlCache: Buffer | undefined;
diff --git a/server/routes/index.ts b/server/routes/index.ts
index 0d304ec50..e32ec2da8 100644
--- a/server/routes/index.ts
+++ b/server/routes/index.ts
@@ -101,14 +101,23 @@ router.get("/locales/:lng.json", async (ctx) => {
await send(ctx, path.join(lng, "translation.json"), {
setHeaders: (res, _, stats) => {
res.setHeader("Last-Modified", formatRFC7231(stats.mtime));
- res.setHeader("Cache-Control", `public, max-age=${7 * Day.seconds}`);
+ res.setHeader(
+ "Cache-Control",
+ env.isDevelopment
+ ? "no-store"
+ : `public, max-age=${7 * Day.seconds}`
+ );
res.setHeader(
"ETag",
crypto.createHash("md5").update(stats.mtime.toISOString()).digest("hex")
);
res.setHeader("Access-Control-Allow-Origin", "*");
},
- root: path.join(__dirname, "../../shared/i18n/locales"),
+ // In dev read the source tree directly so the bind-mounted translation
+ // files (and HMR'd changes) are seen without rebuilding.
+ root: env.isDevelopment
+ ? path.join(__dirname, "../../../shared/i18n/locales")
+ : path.join(__dirname, "../../shared/i18n/locales"),
});
});
diff --git a/vite.config.ts b/vite.config.ts
index 829346243..244308bb3 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -31,7 +31,7 @@ export default () =>
publicDir: "./server/static",
base: (environment.CDN_URL ?? "") + "/static/",
server: {
- port: 3001,
+ port: Number(process.env.PORT_VITE),
host: true,
https: httpsConfig,
allowedHosts: host ? [host] : undefined,
@@ -45,6 +45,27 @@ export default () =>
: { strict: true },
},
plugins: [
+ {
+ // Custom HMR for translation JSON files. Watches shared/i18n/locales
+ // and sends a custom WS event so the client can call
+ // i18n.reloadResources() instead of doing a full page reload.
+ name: "outline-i18n-hmr",
+ apply: "serve",
+ configureServer(server) {
+ const dir = path.resolve(__dirname, "shared/i18n/locales");
+ server.watcher.add(dir);
+ server.watcher.on("change", (file) => {
+ const m = /locales[\\/]([^\\/]+)[\\/]translation\.json$/.exec(file);
+ if (m) {
+ server.ws.send({
+ type: "custom",
+ event: "i18n:update",
+ data: { lng: m[1] },
+ });
+ }
+ });
+ },
+ },
react(),
// https://vite-pwa-org.netlify.app/
VitePWA({
+74
View File
@@ -0,0 +1,74 @@
diff --git a/app/components/Sidebar/components/Version.tsx b/app/components/Sidebar/components/Version.tsx
index f2e8810b2..d4e202447 100644
--- a/app/components/Sidebar/components/Version.tsx
+++ b/app/components/Sidebar/components/Version.tsx
@@ -30,7 +30,7 @@ export default function Version() {
return (
<SidebarLink
target="_blank"
- href="https://github.com/outline/outline/releases"
+ href="https://github.com/flameshikari/outline-ru/releases"
label={
<>
v{currentVersion}
diff --git a/server/env.ts b/server/env.ts
index 34f926029..88f97d515 100644
--- a/server/env.ts
+++ b/server/env.ts
@@ -245,7 +245,7 @@ export class Environment {
*/
@Public
@IsIn(languages)
- public DEFAULT_LANGUAGE = environment.DEFAULT_LANGUAGE ?? "en_US";
+ public DEFAULT_LANGUAGE = environment.DEFAULT_LANGUAGE ?? "ru_RU";
/**
* A comma list of which services should be enabled on this instance defaults to all.
diff --git a/server/utils/getInstallationInfo.ts b/server/utils/getInstallationInfo.ts
index 1d11a426c..00ec42f69 100644
--- a/server/utils/getInstallationInfo.ts
+++ b/server/utils/getInstallationInfo.ts
@@ -2,7 +2,7 @@ import { version } from "../../package.json";
import fetch from "./fetch";
const dockerhubLink =
- "https://hub.docker.com/v2/repositories/outlinewiki/outline";
+ "https://hub.docker.com/v2/repositories/flameshikari/outline-ru";
function isFullReleaseVersion(versionName: string): boolean {
const releaseRegex = /^(version-)?\d+\.\d+\.\d+$/; // Matches "N.N.N" or "version-N.N.N" for dockerhub releases before v0.56.0"
diff --git a/shared/i18n/index.ts b/shared/i18n/index.ts
index e315ef413..b580ec795 100644
--- a/shared/i18n/index.ts
+++ b/shared/i18n/index.ts
@@ -72,6 +72,10 @@ export const languageOptions: LanguageOption[] = [
label: "فارسی (Persian)",
value: "fa_IR",
},
+ {
+ label: "Русский (Russian)",
+ value: "ru_RU",
+ },
{
label: "Svenska (Swedish)",
value: "sv_SE",
diff --git a/shared/utils/date.ts b/shared/utils/date.ts
index 397b2c7a4..a45d418ae 100644
--- a/shared/utils/date.ts
+++ b/shared/utils/date.ts
@@ -23,6 +23,7 @@ import {
ptBR,
pt,
pl,
+ ru,
sv,
tr,
vi,
@@ -175,6 +176,7 @@ const locales = {
pt_BR: ptBR,
pt_PT: pt,
pl_PL: pl,
+ ru_RU: ru,
sv_SE: sv,
tr_TR: tr,
uk_UA: uk,
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env bash
CWD="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
cat $CWD/src/shared/utils/date.ts | \
sed '/^ pl,/a\ \ ru,' | \
sed '/^ pl_PL: pl,/a\ \ ru_RU: ru,' \
> $CWD/shared/utils/date.ts
cat $CWD/src/shared/i18n/index.ts | \
sed '/^export const languageOptions = \[/a\ \ {\n label: "Русский (Russian)",\n value: "ru_RU",\n },' \
> $CWD/shared/i18n/index.ts
+17
View File
@@ -0,0 +1,17 @@
FROM oven/bun:1-alpine
USER root
RUN apk add --no-cache postgresql17 postgresql17-contrib redis su-exec bash
ENV PGDATA=/var/lib/postgresql/data
RUN mkdir -p "$PGDATA" /run/postgresql /var/log \
&& chown -R postgres:postgres "$PGDATA" /run/postgresql
WORKDIR /app
COPY package.json ./
RUN bun install --production
COPY server.js entrypoint.sh avatar.png ./
RUN chmod +x entrypoint.sh
CMD ["./entrypoint.sh"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+40
View File
@@ -0,0 +1,40 @@
#!/bin/sh
set -e
PG_USER="${POSTGRES_USER:-${COMMON:-outline}}"
PG_PASS="${POSTGRES_PASSWORD:-${COMMON:-outline}}"
PG_DB="${POSTGRES_DB:-${COMMON:-outline}}"
# named-volume mount comes up root-owned; reclaim it for postgres
chown -R postgres:postgres "$PGDATA" /run/postgresql
chmod 700 "$PGDATA"
# ---- Postgres ----
if [ ! -s "$PGDATA/PG_VERSION" ]; then
echo "==> initdb in $PGDATA"
su-exec postgres initdb -D "$PGDATA" --username="$PG_USER" \
--auth-local=trust --auth-host=md5 >/dev/null
echo "listen_addresses = '*'" >> "$PGDATA/postgresql.conf"
echo "host all all 0.0.0.0/0 md5" >> "$PGDATA/pg_hba.conf"
fi
echo "==> starting postgres on :5432"
su-exec postgres pg_ctl -D "$PGDATA" -l "$PGDATA/postgres.log" -w start
# ensure password is set (md5 auth needs it for host connections)
su-exec postgres psql -U "$PG_USER" -d postgres -c \
"ALTER ROLE \"$PG_USER\" WITH LOGIN PASSWORD '$PG_PASS';" >/dev/null
# create db idempotently
su-exec postgres psql -U "$PG_USER" -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname='$PG_DB'" | grep -q 1 || \
su-exec postgres createdb -U "$PG_USER" -O "$PG_USER" "$PG_DB"
# ---- Redis ----
echo "==> starting redis on :6379"
redis-server --daemonize yes --bind 0.0.0.0 --port 6379 \
--logfile "" --protected-mode no
# ---- OIDC mock (foreground) ----
echo "==> starting bun OIDC mock on :${PORT_OIDC:-8080}"
exec bun server.js
+13
View File
@@ -0,0 +1,13 @@
{
"name": "mock-oidc",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "bun server.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.0",
"jose": "^5.9.6"
}
}
+230
View File
@@ -0,0 +1,230 @@
// Mock OIDC server with autologin to a single user.
// Compatible with IdentityServer-style clients (/connect/*) and generic OIDC clients.
// NOT FOR PRODUCTION.
import express from 'express';
import cors from 'cors';
import crypto from 'crypto';
import { SignJWT, exportJWK, generateKeyPair } from 'jose';
// ---------- Config ----------
// ISSUER = internal URL used by server-side relying parties (outline backend),
// also signed into JWT `iss` claim.
// PUBLIC = browser-facing URL (host:published-port) used in redirect endpoints.
// PORT = derived from ISSUER URL so a single var sets where we listen.
const ISSUER = process.env.ISSUER || 'http://localhost:8080';
const PUBLIC = process.env.PUBLIC_URL || ISSUER;
const PORT = parseInt(new URL(ISSUER).port || '8080', 10);
const CLIENT_ID = process.env.CLIENT_ID || 'mock-client';
const CLIENT_SECRET = process.env.CLIENT_SECRET || 'mock-secret';
const TOKEN_TTL = parseInt(process.env.TOKEN_TTL || '3600', 10);
const CODE_TTL = parseInt(process.env.CODE_TTL || '60', 10);
const USER = {
sub: process.env.USER_SUB || 'user-1',
email: process.env.USER_EMAIL || 'mail@example.com',
email_verified: true,
name: process.env.USER_NAME || 'Outline',
preferred_username: process.env.USER_USERNAME || 'outline',
given_name: process.env.USER_GIVEN || 'Outline',
family_name: process.env.USER_FAMILY || 'Wiki',
roles: (process.env.USER_ROLES || 'admin,user').split(','),
picture: `${PUBLIC}/avatar.png`,
};
// ---------- Keys ----------
const { publicKey, privateKey } = await generateKeyPair('RS256');
const jwk = { ...(await exportJWK(publicKey)), kid: 'mock-key-1', alg: 'RS256', use: 'sig' };
// ---------- In-memory stores ----------
const codes = new Map();
const tokens = new Map();
const refreshTokens = new Map();
// ---------- Helpers ----------
const now = () => Math.floor(Date.now() / 1000);
const signJwt = (payload, audience, ttl = TOKEN_TTL) =>
new SignJWT(payload)
.setProtectedHeader({ alg: 'RS256', kid: jwk.kid, typ: 'JWT' })
.setIssuer(ISSUER)
.setSubject(USER.sub)
.setAudience(audience)
.setIssuedAt(now())
.setExpirationTime(now() + ttl)
.setJti(crypto.randomBytes(16).toString('hex'))
.sign(privateKey);
const extractClientCreds = (req) => {
const auth = req.headers.authorization;
if (auth?.startsWith('Basic ')) {
const decoded = Buffer.from(auth.slice(6), 'base64').toString();
const idx = decoded.indexOf(':');
return {
clientId: decodeURIComponent(decoded.slice(0, idx)),
clientSecret: decodeURIComponent(decoded.slice(idx + 1)),
};
}
return { clientId: req.body.client_id, clientSecret: req.body.client_secret };
};
const pkceMatches = (verifier, challenge, method) => {
if (!verifier) return false;
if (method === 'S256') return crypto.createHash('sha256').update(verifier).digest('base64url') === challenge;
return verifier === challenge;
};
// ---------- App ----------
const app = express();
app.use(cors({ origin: true, credentials: true }));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use((req, _res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// ---------- Discovery + JWKS ----------
app.get('/.well-known/openid-configuration', (_req, res) => res.json({
issuer: ISSUER,
authorization_endpoint: `${PUBLIC}/connect/authorize`,
token_endpoint: `${ISSUER}/connect/token`,
userinfo_endpoint: `${ISSUER}/connect/userinfo`,
end_session_endpoint: `${PUBLIC}/connect/endsession`,
jwks_uri: `${ISSUER}/.well-known/jwks`,
response_types_supported: ['code'],
response_modes_supported: ['query', 'fragment'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
scopes_supported: ['openid', 'profile', 'email', 'offline_access'],
token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic', 'none'],
claims_supported: ['sub', 'email', 'email_verified', 'name', 'preferred_username', 'given_name', 'family_name', 'roles'],
code_challenge_methods_supported: ['S256', 'plain'],
grant_types_supported: ['authorization_code', 'refresh_token'],
}));
app.get(['/.well-known/jwks', '/.well-known/openid-configuration/jwks', '/jwks'],
(_req, res) => res.json({ keys: [jwk] }));
// ---------- /authorize: AUTOLOGIN ----------
app.get(['/connect/authorize', '/authorize'], (req, res) => {
const { redirect_uri, state, nonce, scope, response_type,
client_id, code_challenge, code_challenge_method } = req.query;
const fail = (status, error, description) =>
res.status(status).json({ error, error_description: description });
if (response_type !== 'code') return fail(400, 'unsupported_response_type', 'response_type must be "code"');
if (!client_id) return fail(400, 'invalid_request', 'client_id is required');
if (!redirect_uri) return fail(400, 'invalid_request', 'redirect_uri is required');
const code = crypto.randomBytes(32).toString('hex');
codes.set(code, {
client_id, redirect_uri, nonce,
scope: scope || 'openid',
code_challenge, code_challenge_method,
expires_at: Date.now() + CODE_TTL * 1000,
});
const url = new URL(redirect_uri);
url.searchParams.set('code', code);
if (state) url.searchParams.set('state', state);
console.log(` -> autologin: redirect to ${url.toString()}`);
res.redirect(url.toString());
});
// ---------- /token ----------
app.post(['/connect/token', '/token'], async (req, res) => {
const { grant_type } = req.body;
if (grant_type === 'authorization_code') {
const { code, redirect_uri, code_verifier } = req.body;
const { clientId, clientSecret } = extractClientCreds(req);
const ctx = codes.get(code);
codes.delete(code);
if (!ctx) return res.status(400).json({ error: 'invalid_grant', error_description: 'unknown code' });
if (ctx.expires_at < Date.now()) return res.status(400).json({ error: 'invalid_grant', error_description: 'code expired' });
if (ctx.redirect_uri !== redirect_uri) return res.status(400).json({ error: 'invalid_grant', error_description: 'redirect_uri mismatch' });
if (ctx.code_challenge) {
if (!pkceMatches(code_verifier, ctx.code_challenge, ctx.code_challenge_method))
return res.status(400).json({ error: 'invalid_grant', error_description: 'PKCE verification failed' });
} else if (clientId !== CLIENT_ID || clientSecret !== CLIENT_SECRET) {
return res.status(401).json({ error: 'invalid_client' });
}
const aud = clientId || CLIENT_ID;
const id_token = await signJwt({ ...USER, nonce: ctx.nonce, auth_time: now() }, aud);
const access_token = await signJwt({ scope: ctx.scope, client_id: aud, ...USER }, aud);
tokens.set(access_token, USER);
const response = { access_token, id_token, token_type: 'Bearer', expires_in: TOKEN_TTL, scope: ctx.scope };
if (ctx.scope.split(' ').includes('offline_access')) {
const refresh_token = crypto.randomBytes(32).toString('hex');
refreshTokens.set(refresh_token, { client_id: aud, scope: ctx.scope });
response.refresh_token = refresh_token;
}
return res.json(response);
}
if (grant_type === 'refresh_token') {
const { refresh_token } = req.body;
const ctx = refreshTokens.get(refresh_token);
if (!ctx) return res.status(400).json({ error: 'invalid_grant' });
const id_token = await signJwt({ ...USER, auth_time: now() }, ctx.client_id);
const access_token = await signJwt({ scope: ctx.scope, client_id: ctx.client_id, ...USER }, ctx.client_id);
tokens.set(access_token, USER);
return res.json({
access_token, id_token, refresh_token,
token_type: 'Bearer', expires_in: TOKEN_TTL, scope: ctx.scope,
});
}
res.status(400).json({ error: 'unsupported_grant_type' });
});
// ---------- /userinfo ----------
app.all(['/connect/userinfo', '/userinfo'], (req, res) => {
const auth = req.headers.authorization;
const user = auth?.startsWith('Bearer ') ? tokens.get(auth.slice(7)) : null;
if (!user) return res.status(401).json({ error: 'invalid_token' });
res.json(user);
});
// ---------- /endsession ----------
app.get(['/connect/endsession', '/logout'], (req, res) => {
const { post_logout_redirect_uri, state } = req.query;
if (post_logout_redirect_uri) {
const url = new URL(post_logout_redirect_uri);
if (state) url.searchParams.set('state', state);
return res.redirect(url.toString());
}
res.type('html').send('<h1>Logged out</h1>');
});
// ---------- Index + health + 404 ----------
app.get('/', (_req, res) => res.type('html').send(`<h1>OIDC Mock Server</h1>
<p>Autologin as <strong>${USER.email}</strong> (sub: <code>${USER.sub}</code>)</p>
<ul>
<li><a href="/.well-known/openid-configuration">/.well-known/openid-configuration</a></li>
<li><a href="/.well-known/jwks">/.well-known/jwks</a></li>
<li><a href="/connect/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=http://localhost:3000/callback&scope=openid+profile+email&state=test">Simulate /authorize</a></li>
</ul>
<p>Client ID: <code>${CLIENT_ID}</code> &middot; Client Secret: <code>${CLIENT_SECRET}</code></p>`));
app.get('/avatar.png', (_req, res) => res.sendFile('avatar.png', { root: import.meta.dirname }));
app.get('/health', (_req, res) => res.json({ status: 'ok', issuer: ISSUER }));
app.use((req, res) => res.status(404).json({
error: 'not_found', path: req.path,
hint: 'See /.well-known/openid-configuration',
}));
// ---------- Start ----------
app.listen(PORT, () => {
console.log(`Mock OIDC listening on :${PORT} issuer=${ISSUER} public=${PUBLIC} client_id=${CLIENT_ID} autologin=${USER.email} (sub=${USER.sub}, roles=${USER.roles.join(',')})`);
});
-90
View File
@@ -1,90 +0,0 @@
// Note: Updating the available languages? Make sure to also update the
// locales array in shared/utils/date.ts to enable translation for timestamps.
export const languageOptions = [
{
label: "Русский (Russian)",
value: "ru_RU",
},
{
label: "English (US)",
value: "en_US",
},
{
label: "Čeština (Czech)",
value: "cs_CZ",
},
{
label: "简体中文 (Chinese, Simplified)",
value: "zh_CN",
},
{
label: "繁體中文 (Chinese, Traditional)",
value: "zh_TW",
},
{
label: "Deutsch (German)",
value: "de_DE",
},
{
label: "Español (Spanish)",
value: "es_ES",
},
{
label: "Français (French)",
value: "fr_FR",
},
{
label: "Italiano (Italian)",
value: "it_IT",
},
{
label: "日本語 (Japanese)",
value: "ja_JP",
},
{
label: "한국어 (Korean)",
value: "ko_KR",
},
{
label: "Nederland (Dutch, Netherlands)",
value: "nl_NL",
},
{
label: "Norsk Bokmål (Norwegian)",
value: "nb_NO",
},
{
label: "Português (Portuguese, Brazil)",
value: "pt_BR",
},
{
label: "Português (Portuguese, Portugal)",
value: "pt_PT",
},
{
label: "Polskie (Polish)",
value: "pl_PL",
},
{
label: "فارسی (Persian)",
value: "fa_IR",
},
{
label: "Svenska (Swedish)",
value: "sv_SE",
},
{
label: "Türkçe (Turkish)",
value: "tr_TR",
},
{
label: "Українська (Ukrainian)",
value: "uk_UA",
},
{
label: "Tiếng Việt (Vietnamese)",
value: "vi_VN",
},
];
export const languages = languageOptions.map((i) => i.value);
File diff suppressed because it is too large Load Diff
-198
View File
@@ -1,198 +0,0 @@
/* eslint-disable import/no-duplicates */
import {
Locale,
addSeconds,
formatDistanceToNow,
subDays,
subMonths,
subWeeks,
subYears,
} from "date-fns";
import {
cs,
de,
enUS,
es,
faIR,
fr,
it,
ja,
ko,
nb,
nl,
ptBR,
pt,
pl,
ru,
sv,
tr,
vi,
uk,
zhCN,
zhTW,
} from "date-fns/locale";
import type { DateFilter } from "../types";
export function subtractDate(date: Date, period: DateFilter) {
switch (period) {
case "day":
return subDays(date, 1);
case "week":
return subWeeks(date, 1);
case "month":
return subMonths(date, 1);
case "year":
return subYears(date, 1);
default:
return date;
}
}
/**
* Returns a humanized relative time string for the given date.
*
* @param date The date to convert
* @param options The options to pass to date-fns
* @returns The relative time string
*/
export function dateToRelative(
date: Date | number,
options?: {
includeSeconds?: boolean;
addSuffix?: boolean;
locale?: Locale | undefined;
shorten?: boolean;
}
) {
const now = new Date();
const parsedDateTime = new Date(date);
// Protect against "in less than a minute" when users computer clock is off.
const normalizedDateTime =
parsedDateTime > now && parsedDateTime < addSeconds(now, 60)
? now
: parsedDateTime;
const output = formatDistanceToNow(normalizedDateTime, options);
// Some tweaks to make english language shorter.
if (options?.shorten) {
return output
.replace("about", "")
.replace("less than a minute ago", "just now")
.replace("minute", "min");
}
return output;
}
/**
* Converts a locale string from Unicode CLDR format to BCP47 format.
*
* @param locale The locale string to convert
* @returns The converted locale string
*/
export function unicodeCLDRtoBCP47(locale: string) {
return locale.replace("_", "-").replace("root", "und");
}
/**
* Converts a locale string from BCP47 format to Unicode CLDR format.
*
* @param locale The locale string to convert
* @returns The converted locale string
*/
export function unicodeBCP47toCLDR(locale: string) {
return locale.replace("-", "_").replace("und", "root");
}
/**
* Converts a locale string from Unicode CLDR format to ISO 639 format.
*
* @param locale The locale string to convert
* @returns The converted locale string
*/
export function unicodeCLDRtoISO639(locale: string) {
return locale.split("_")[0];
}
/**
* Returns the current date as a string formatted depending on current locale.
*
* @returns The current date
*/
export function getCurrentDateAsString(locale?: Intl.LocalesArgument) {
return new Date().toLocaleDateString(locale, {
year: "numeric",
month: "long",
day: "numeric",
});
}
/**
* Returns the current time as a string formatted depending on current locale.
*
* @returns The current time
*/
export function getCurrentTimeAsString(locale?: Intl.LocalesArgument) {
return new Date().toLocaleTimeString(locale, {
hour: "numeric",
minute: "numeric",
});
}
/**
* Returns the current date and time as a string formatted depending on current
* locale.
*
* @returns The current date and time
*/
export function getCurrentDateTimeAsString(locale?: Intl.LocalesArgument) {
return new Date().toLocaleString(locale, {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
});
}
const locales = {
cs_CZ: cs,
de_DE: de,
en_US: enUS,
es_ES: es,
fa_IR: faIR,
fr_FR: fr,
it_IT: it,
ja_JP: ja,
ko_KR: ko,
nb_NO: nb,
nl_NL: nl,
pt_BR: ptBR,
pt_PT: pt,
pl_PL: pl,
ru_RU: ru,
sv_SE: sv,
tr_TR: tr,
uk_UA: uk,
vi_VN: vi,
zh_CN: zhCN,
zh_TW: zhTW,
};
/**
* Returns the date-fns locale object for the given user language preference.
*
* @param language The user language
* @returns The date-fns locale.
*/
export function dateLocale(language: string | null | undefined) {
return language ? locales[language] : undefined;
}
export { locales };
Submodule src deleted from ce85b8f94d
@@ -3,19 +3,24 @@
import json
import os
def workdir(path):
def resolve(path):
basepath = os.path.abspath(os.path.dirname(__file__))
abspath = os.path.abspath(basepath + '/' + path)
return abspath
en_json_path = workdir('../src/shared/i18n/locales/en_US/translation.json')
ru_json_path = workdir('../shared/i18n/locales/ru_RU/translation.json')
out_json_path = workdir('./translation.json')
en_json_path = resolve('../outline/shared/i18n/locales/en_US/translation.json')
ru_json_path = resolve('./ru.json')
out_json_name = 'tmp.json'
out_json_path = resolve(out_json_name)
translated_lines = {}
untranslated_lines = {}
exception_lines = {}
placeholder = '[NOT TRANSLATED]'
with open(en_json_path) as target:
en_json = json.load(target)
@@ -23,21 +28,35 @@ with open(ru_json_path) as target:
ru_json = json.load(target)
for key, value in en_json.items():
if key in ru_json.keys():
translated_lines[key] = ru_json[key]
else:
untranslated_lines[key] = en_json[key]
# skip x_plural strings
if key.endswith('_plural'):
continue
# keep translated strings
elif key in ru_json.keys():
translated_lines[key] = ru_json[key]
# process plurals
elif key in en_json.keys() and f'{key}_plural' in en_json.keys():
for i in range(0, 3):
plural = f'{key}_{i}'
if plural in ru_json.keys():
translated_lines[plural] = ru_json[plural]
else:
untranslated_lines[plural] = placeholder
else:
untranslated_lines[key] = placeholder
for key, value in ru_json.items():
if key == value:
exception_lines[key] = value
out_json = {**translated_lines, **untranslated_lines}
if (exception_lines):
print('Потенциально непереведённые строки или исключения:')
print('Исключения:')
print(json.dumps(exception_lines, indent=2, ensure_ascii=False))
print()
@@ -49,4 +68,4 @@ if (untranslated_lines):
with open(out_json_path, 'w') as target:
obj = json.dumps(out_json, indent=2, ensure_ascii=False)
target.write(obj + '\n')
print('Переведенные и непереведенные строки смержены в translation.json')
print(f'Переведенные и непереведенные строки смержены в {out_json_name}')
+1811
View File
File diff suppressed because it is too large Load Diff