mirror of
https://github.com/flameshikari/outline-ru.git
synced 2026-06-13 12:15:15 +03:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e026250893 | |||
| d2be66831d | |||
| 49472ac801 | |||
| a81bd3bd17 | |||
| f22d5952bb | |||
| 02bdd461b4 | |||
| d0abf84aa8 | |||
| b3bda3622c | |||
| 856cf4b0c9 | |||
| 0bc1c14a9c | |||
| 2c681f14f2 | |||
| 2914b54933 | |||
| 076879fd63 | |||
| fbe5d1adb1 | |||
| b7c7d5a0ac | |||
| 2fe50b5784 | |||
| 08aef9b911 | |||
| d963ac76bf | |||
| afedb4b0ca | |||
| 2ccc82c02e | |||
| daa7561aff | |||
| 7bcf8279e2 | |||
| bc93c415d8 | |||
| 7cbc22e1bf | |||
| 73dd19e49b | |||
| b9bfdc024f | |||
| 56c8631c5a | |||
| 74c29e1934 | |||
| d3399ba110 | |||
| c6e5a87421 | |||
| 0675c06f23 | |||
| ddb2decad0 | |||
| 3a3728b9bd | |||
| be856bfbdc | |||
| 82256f4ada | |||
| c7d513f03e | |||
| 8d9d6c7f14 | |||
| c794997b26 | |||
| 4485a10514 | |||
| b129a74d39 | |||
| d22c3ab04c | |||
| cf2d26b011 | |||
| 2c34b016fe | |||
| 16ddf6f2b6 | |||
| 032263ec5d | |||
| e098d1a7df | |||
| 5204b57626 | |||
| 2751858e48 | |||
| bef3b69a5e | |||
| af60692963 | |||
| fdb2cc6bc5 | |||
| 0070936e30 |
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
COMPOSE_PROFILES=dev
|
||||
|
||||
# PORT=10240
|
||||
# PORT_OIDC=10241
|
||||
# PORT_VITE=10242
|
||||
# SECRET=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef # openssl rand -hex 32
|
||||
Executable
+11
@@ -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
|
||||
Executable
+31
@@ -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)
|
||||
+36
-27
@@ -6,12 +6,20 @@ on:
|
||||
- master
|
||||
- dev
|
||||
paths:
|
||||
- .github/workflows/*
|
||||
- shared/**
|
||||
- src/**
|
||||
- .github/workflows/**
|
||||
- outline/**
|
||||
- translation/ru.json
|
||||
- Dockerfile
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: outline
|
||||
|
||||
env:
|
||||
DOCKER_BUILD_CHECKS_ANNOTATIONS: false
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build [${{ matrix.arch }}]
|
||||
@@ -26,26 +34,23 @@ jobs:
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
# - ppc64le
|
||||
# - s390x
|
||||
# - arm/v7
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
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: Login to GitHub CR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -54,10 +59,12 @@ jobs:
|
||||
- name: Set Version
|
||||
id: version
|
||||
run: |
|
||||
echo "version=$(jq -r '.version' src/package.json)" | tee -a $GITHUB_OUTPUT
|
||||
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@v5
|
||||
uses: docker/metadata-action@v6
|
||||
id: metadata
|
||||
with:
|
||||
images: |
|
||||
@@ -65,25 +72,26 @@ jobs:
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build Image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
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
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload Digests
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-linux-${{ matrix.arch }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -94,45 +102,45 @@ jobs:
|
||||
name: Publish
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
packages: write
|
||||
env:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
steps:
|
||||
- name: Download Digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub CR
|
||||
uses: docker/login-action@v3
|
||||
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@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Set Metadata
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=${{ env.version }}
|
||||
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
|
||||
@@ -143,9 +151,10 @@ jobs:
|
||||
$(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 }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
scripts/translation.json
|
||||
translation/*.json
|
||||
!translation/ru.json
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
[submodule "src"]
|
||||
path = src
|
||||
[submodule "outline"]
|
||||
path = outline
|
||||
url = https://github.com/outline/outline.git
|
||||
|
||||
+41
-41
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -1,103 +1,25 @@
|
||||
Business Source License 1.1
|
||||
MIT License
|
||||
|
||||
Parameters
|
||||
Copyright (c) 2025 flameshikari
|
||||
|
||||
Licensor: General Outline, Inc.
|
||||
Licensed Work: Outline 0.82.0-0
|
||||
The Licensed Work is (c) 2025 General Outline, Inc.
|
||||
Additional Use Grant: You may make use of the Licensed Work, provided that
|
||||
you may not use the Licensed Work for a Document
|
||||
Service.
|
||||
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:
|
||||
|
||||
A “Document Service” is a commercial offering that
|
||||
allows third parties (other than your employees and
|
||||
contractors) to access the functionality of the
|
||||
Licensed Work by creating teams and documents
|
||||
controlled by such third parties.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
Change Date: 2029-01-31
|
||||
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.
|
||||
|
||||
Change License: Apache License, Version 2.0
|
||||
---
|
||||
|
||||
For information about alternative licensing arrangements for the Software,
|
||||
please visit: https://www.getoutline.com
|
||||
|
||||
Notice
|
||||
|
||||
The Business Source License (this document, or the “License”) is not an Open
|
||||
Source license. However, the Licensed Work will eventually be made available
|
||||
under an Open Source License, as stated in this License.
|
||||
|
||||
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
||||
“Business Source License” is a trademark of MariaDB Corporation Ab.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
Business Source License 1.1
|
||||
|
||||
Terms
|
||||
|
||||
The Licensor hereby grants you the right to copy, modify, create derivative
|
||||
works, redistribute, and make non-production use of the Licensed Work. The
|
||||
Licensor may make an Additional Use Grant, above, permitting limited
|
||||
production use.
|
||||
|
||||
Effective on the Change Date, or the fourth anniversary of the first publicly
|
||||
available distribution of a specific version of the Licensed Work under this
|
||||
License, whichever comes first, the Licensor hereby grants you rights under
|
||||
the terms of the Change License, and the rights granted in the paragraph
|
||||
above terminate.
|
||||
|
||||
If your use of the Licensed Work does not comply with the requirements
|
||||
currently in effect as described in this License, you must purchase a
|
||||
commercial license from the Licensor, its affiliated entities, or authorized
|
||||
resellers, or you must refrain from using the Licensed Work.
|
||||
|
||||
All copies of the original and modified Licensed Work, and derivative works
|
||||
of the Licensed Work, are subject to this License. This License applies
|
||||
separately for each version of the Licensed Work and the Change Date may vary
|
||||
for each version of the Licensed Work released by Licensor.
|
||||
|
||||
You must conspicuously display this License on each original or modified copy
|
||||
of the Licensed Work. If you receive the Licensed Work in original or
|
||||
modified form from a third party, the terms and conditions set forth in this
|
||||
License apply to your use of that work.
|
||||
|
||||
Any use of the Licensed Work in violation of this License will automatically
|
||||
terminate your rights under this License for the current and all other
|
||||
versions of the Licensed Work.
|
||||
|
||||
This License does not grant you any right in any trademark or logo of
|
||||
Licensor or its affiliates (provided that you may use a trademark or logo of
|
||||
Licensor as expressly required by this License).
|
||||
|
||||
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
||||
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
||||
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
||||
TITLE.
|
||||
|
||||
MariaDB hereby grants you permission to use this License’s text to license
|
||||
your works, and to refer to it using the trademark “Business Source License”,
|
||||
as long as you comply with the Covenants of Licensor below.
|
||||
|
||||
Covenants of Licensor
|
||||
|
||||
In consideration of the right to use this License’s text and the “Business
|
||||
Source License” name and trademark, Licensor covenants to MariaDB, and to all
|
||||
other recipients of the licensed work to be provided by Licensor:
|
||||
|
||||
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
||||
or a license that is compatible with GPL Version 2.0 or a later version,
|
||||
where “compatible” means that software provided under the Change License can
|
||||
be included in a program with software provided under GPL Version 2.0 or a
|
||||
later version. Licensor may specify additional Change Licenses without
|
||||
limitation.
|
||||
|
||||
2. To either: (a) specify an additional grant of rights to use that does not
|
||||
impose any additional restriction on the right granted in this License, as
|
||||
the Additional Use Grant; or (b) insert the text “None”.
|
||||
|
||||
3. To specify a Change Date.
|
||||
|
||||
4. Not to modify this License in any other way.
|
||||
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.
|
||||
@@ -1,5 +1,3 @@
|
||||

|
||||
|
||||
# 📚 [Outline](https://github.com/outline/outline) с русским переводом [](https://github.com/flameshikari/outline-ru/actions) [](https://github.com/flameshikari/outline-ru/releases/latest)
|
||||
|
||||
## ❓ Зачем
|
||||
@@ -8,30 +6,25 @@
|
||||
|
||||
## 📝 Примечания
|
||||
|
||||
За основу взят перевод из [данного коммита](https://github.com/outline/outline/commit/228d1faa9fd3cbb82409d98e1443fed65adc5715), который впоследствии улучшается и переводится здесь.
|
||||
|
||||
Буду рад помощи в улучшении перевода или сборки; сообщить о некорректном переводе можно [здесь](https://github.com/flameshikari/outline-ru/discussions/8).
|
||||
|
||||
Из доступных архитектур контейнера имеются только `amd64` и `arm64`.
|
||||
|
||||
## ⚠️ Дисклеймер
|
||||
|
||||
Данное программное обеспечение предоставляется «как есть», без каких-либо гарантий, явно выраженных или подразумеваемых, включая гарантии товарной пригодности, соответствия по его конкретному назначению и отсутствия нарушений, но не ограничиваясь ими. Ни в каком случае авторы или правообладатели не несут ответственности по каким-либо искам, за ущерб или по иным требованиям, в том числе, при действии контракта, деликте или иной ситуации, возникшим из-за использования программного обеспечения или иных действий с программным обеспечением.
|
||||
- образ доступен в [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)
|
||||
|
||||
## 🐳 Установка
|
||||
|
||||
> Перед установкой **ОБЯЗАТЕЛЬНО** прочтите [про бэкапы перед обновлением](https://docs.getoutline.com/s/hosting/doc/backups-KZtPOADCHG).
|
||||
> [!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/tags)), а также задайте переменную `DEFAULT_LANGUAGE=ru_RU` в [docker.env](https://github.com/outline/outline/blob/main/.env.sample) или в `environments` (в зависимости от вашей конфигурации).
|
||||
Следуйте [официальной инструкции](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:
|
||||
@@ -40,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) через пару секунд.
|
||||
|
||||
@@ -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
@@ -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({
|
||||
@@ -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,
|
||||
@@ -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: LanguageOption\[\] = \[/a\ \ {\n label: "Русский (Russian)",\n value: "ru_RU",\n },' \
|
||||
> $CWD/shared/i18n/index.ts
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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> · 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(',')})`);
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { locales } from "../utils/date";
|
||||
|
||||
type LanguageOption = {
|
||||
label: string;
|
||||
value: keyof typeof locales;
|
||||
};
|
||||
|
||||
// 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: LanguageOption[] = [
|
||||
{
|
||||
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
@@ -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: keyof typeof locales | undefined | null) {
|
||||
return language ? locales[language] : undefined;
|
||||
}
|
||||
|
||||
export { locales };
|
||||
-1
Submodule src deleted from 7bc687b6bf
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user