├── .node-version
├── .eslintignore
├── .stylelintignore
├── src
├── __mocks__
│ ├── file.ts
│ └── axios.ts
├── favicon.ico
├── components
│ ├── footer
│ │ ├── img
│ │ │ ├── stsi.gif
│ │ │ ├── github.png
│ │ │ ├── GCI_logo.png
│ │ │ ├── logouapp.gif
│ │ │ ├── gplv3-88x31.png
│ │ │ ├── logomitc120.jpg
│ │ │ ├── logo_bytemark.gif
│ │ │ ├── prompsit150x52.png
│ │ │ ├── logo_mae_ro_75pc.jpg
│ │ │ └── cc-by-sa-3.0-88x31.png
│ │ ├── footer.css
│ │ ├── DownloadModal.tsx
│ │ ├── DocumentationModal.tsx
│ │ ├── ContactModal.tsx
│ │ ├── __tests__
│ │ │ └── index.test.tsx
│ │ ├── AboutModal.tsx
│ │ └── index.tsx
│ ├── navbar
│ │ ├── Apertium_box_white_small.embed.png
│ │ ├── __tests__
│ │ │ └── index.test.tsx
│ │ └── index.tsx
│ ├── __tests__
│ │ ├── ErrorAlert.test.tsx
│ │ ├── LocaleSelector.test.tsx
│ │ ├── WithLocale.test.tsx
│ │ ├── Sandbox.test.tsx
│ │ └── WithInstallationAlert.test.tsx
│ ├── ErrorAlert.tsx
│ ├── LocaleSelector.tsx
│ ├── translator
│ │ ├── translator.css
│ │ ├── index.ts
│ │ ├── __tests__
│ │ │ ├── WithSortedLanguages.test.tsx
│ │ │ └── TranslationOptions.test.tsx
│ │ ├── WithSortedLanguages.tsx
│ │ └── TranslationOptions.tsx
│ ├── WithLocale.tsx
│ ├── WithInstallationAlert.tsx
│ └── Sandbox.tsx
├── testEnvSetup.ts
├── app.css
├── util
│ ├── strings.ts
│ ├── index.ts
│ ├── __tests__
│ │ ├── url.test.ts
│ │ ├── index.test.ts
│ │ ├── languages.test.ts
│ │ ├── localization.test.tsx
│ │ └── useLocalStorage.test.ts
│ ├── url.ts
│ ├── useLocalStorage.ts
│ └── localization.ts
├── globals.d.ts
├── strings
│ ├── Makefile
│ ├── progresstable.sh
│ ├── locales.json
│ ├── README.md
│ ├── ron.json
│ ├── zho.json
│ ├── eus.json
│ ├── ava.json
│ ├── por.json
│ ├── sme.json
│ ├── spa.json
│ ├── kir.json
│ ├── heb.json
│ ├── tha.json
│ ├── kaz.json
│ ├── uig.json
│ ├── rus.json
│ ├── oci.json
│ ├── swe.json
│ ├── srd.json
│ ├── ara.json
│ ├── uzb.json
│ ├── fin.json
│ └── nno.json
├── context.ts
├── index.tsx
├── types.ts
├── __tests__
│ ├── index.test.tsx
│ └── App.test.tsx
├── index.html
├── rtl.css
├── testSetup.ts
└── App.tsx
├── .gitattributes
├── .prettierrc.toml
├── .dockerignore
├── .stylelintrc.json
├── .gitignore
├── .prettierignore
├── tsconfig.json
├── Dockerfile
├── .github
└── workflows
│ ├── build.yml
│ └── check.yml
├── jest.config.ts
├── config.ts
├── .eslintrc.json
├── package.json
└── README.md
/.node-version:
--------------------------------------------------------------------------------
1 | 14.19.1
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /dist/
3 | /coverage/
4 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | src/bootstrap.css
2 | src/**/*.tsx
3 |
--------------------------------------------------------------------------------
/src/__mocks__/file.ts:
--------------------------------------------------------------------------------
1 | export default 'http://placekitten.com/200/300';
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.prettierrc.toml:
--------------------------------------------------------------------------------
1 | printWidth = 120
2 | singleQuote = true
3 | trailingComma = 'all'
4 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | tsconfig.tsbuildinfo
3 | coverage/
4 | dist/
5 | node_modules/
6 |
--------------------------------------------------------------------------------
/src/__mocks__/axios.ts:
--------------------------------------------------------------------------------
1 | import mockAxios from 'jest-mock-axios';
2 | export default mockAxios;
3 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /yarn-error.log
3 | /meta.json
4 | /tsconfig.tsbuildinfo
5 | /coverage/
6 | /dist/
7 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /dist/
2 | /coverage/
3 | /node_modules/
4 | /src/strings/
5 | /src/bootstrap.css
6 | /meta.json
7 |
--------------------------------------------------------------------------------
/src/components/footer/img/stsi.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/stsi.gif
--------------------------------------------------------------------------------
/src/components/footer/img/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/github.png
--------------------------------------------------------------------------------
/src/components/footer/img/GCI_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/GCI_logo.png
--------------------------------------------------------------------------------
/src/components/footer/img/logouapp.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/logouapp.gif
--------------------------------------------------------------------------------
/src/components/footer/img/gplv3-88x31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/gplv3-88x31.png
--------------------------------------------------------------------------------
/src/components/footer/img/logomitc120.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/logomitc120.jpg
--------------------------------------------------------------------------------
/src/components/footer/img/logo_bytemark.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/logo_bytemark.gif
--------------------------------------------------------------------------------
/src/components/footer/img/prompsit150x52.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/prompsit150x52.png
--------------------------------------------------------------------------------
/src/components/footer/img/logo_mae_ro_75pc.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/logo_mae_ro_75pc.jpg
--------------------------------------------------------------------------------
/src/components/footer/img/cc-by-sa-3.0-88x31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/footer/img/cc-by-sa-3.0-88x31.png
--------------------------------------------------------------------------------
/src/testEnvSetup.ts:
--------------------------------------------------------------------------------
1 | import mockAxios from 'jest-mock-axios';
2 |
3 | afterEach(() => {
4 | mockAxios.reset();
5 | window.localStorage.clear();
6 | });
7 |
--------------------------------------------------------------------------------
/src/components/navbar/Apertium_box_white_small.embed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apertium/apertium-html-tools/HEAD/src/components/navbar/Apertium_box_white_small.embed.png
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | .blurred {
2 | opacity: 0.5;
3 | transition: all 1s ease;
4 | }
5 |
6 | .btn-primary {
7 | background-color: #446e9b;
8 | border-color: #446e9b;
9 | color: #fff;
10 | }
11 |
12 | .btn-primary:hover {
13 | background-color: #385a7f;
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "strict": true,
5 | "incremental": true,
6 | "lib": ["es2019", "dom"],
7 | "esModuleInterop": true,
8 | "resolveJsonModule": true,
9 | "downlevelIteration": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-buster-slim
2 | LABEL maintainer sushain@skc.name
3 | WORKDIR /root
4 |
5 | RUN apt-get -qq update && \
6 | apt-get -qq install --no-install-recommends git
7 |
8 | COPY package.json .
9 | COPY yarn.lock .
10 | RUN yarn install --dev
11 |
12 | COPY . .
13 |
14 | ENTRYPOINT ["yarn"]
15 | CMD ["build", "--prod"]
16 |
--------------------------------------------------------------------------------
/src/util/strings.ts:
--------------------------------------------------------------------------------
1 | export type Strings = {
2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3 | // @ts-ignore
4 | readonly '@langNames': Record;
5 | readonly [id: string]: string;
6 | };
7 |
8 | // eslint-disable-next-line
9 | export const PRELOADED_STRINGS: Readonly> = (window as any).PRELOADED_STRINGS;
10 |
--------------------------------------------------------------------------------
/src/globals.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png' {
2 | const url: string;
3 | export default url;
4 | }
5 |
6 | declare module '*.gif' {
7 | const url: string;
8 | export default url;
9 | }
10 |
11 | declare module '*.jpg' {
12 | const url: string;
13 | export default url;
14 | }
15 |
16 | declare module '*.svg' {
17 | const url: string;
18 | export default url;
19 | }
20 |
--------------------------------------------------------------------------------
/src/strings/Makefile:
--------------------------------------------------------------------------------
1 | all: cleanup update-readme
2 |
3 | cleanup: $(shell find . -name '*.json' | egrep '/[a-z]{3}\.json')
4 | ./localisation-tools.py cleanup $?
5 |
6 | update-readme: README.md
7 | sed -n "//q;p" $^ > $^.tmp
8 | echo "" >> $^.tmp
9 | ./progresstable.sh md >> $^.tmp
10 | mv $^.tmp $^
11 |
12 | test: README.md
13 | git diff --exit-code .
14 |
--------------------------------------------------------------------------------
/src/components/footer/footer.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | #react-mount {
4 | height: 100%; /* The html and body elements cannot have any padding or margin. */
5 | }
6 |
7 | @media print {
8 | #footer {
9 | display: none;
10 | }
11 | }
12 |
13 | .version:hover,
14 | .version:active,
15 | .version:focus {
16 | text-decoration: none;
17 | }
18 |
19 | .footer-link {
20 | font-size: 1rem;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/__tests__/ErrorAlert.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import ErrorAlert from '../ErrorAlert';
5 |
6 | it('shows generic errors', () => {
7 | render( );
8 |
9 | const error = screen.getByRole('alert');
10 | expect(error.textContent).toMatchInlineSnapshot(`" Error: Network Error"`);
11 | });
12 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import Config from '../config';
4 | import { PRELOADED_STRINGS } from './util/strings';
5 | import { apyFetch } from './util';
6 |
7 | const LocaleContext = React.createContext(Config.defaultLocale);
8 | const ConfigContext = React.createContext(Config);
9 | const StringsContext = React.createContext(PRELOADED_STRINGS);
10 | const APyContext = React.createContext(apyFetch);
11 |
12 | export { APyContext, ConfigContext, LocaleContext, StringsContext };
13 |
--------------------------------------------------------------------------------
/src/util/index.ts:
--------------------------------------------------------------------------------
1 | import * as queryString from 'query-string';
2 | import axios, { AxiosPromise, CancelTokenSource } from 'axios';
3 |
4 | export const apyFetch = (path: string, params?: Record): [CancelTokenSource, AxiosPromise] => {
5 | const source = axios.CancelToken.source();
6 |
7 | return [
8 | source,
9 | axios.post(path, params ? queryString.stringify(params) : '', {
10 | headers: {
11 | 'Content-Type': 'application/x-www-form-urlencoded',
12 | },
13 | cancelToken: source.token,
14 | validateStatus: (status) => status === 200,
15 | }),
16 | ];
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/footer/DownloadModal.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Modal, { ModalProps } from 'react-bootstrap/Modal';
3 |
4 | import { useLocalization } from '../../util/localization';
5 |
6 | const DownloadModal = (props: ModalProps): React.ReactElement => {
7 | const { t } = useLocalization();
8 |
9 | return (
10 |
11 |
12 | {t('Apertium_Downloads')}
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default DownloadModal;
20 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | - push
4 | - pull_request
5 | jobs:
6 | build:
7 | runs-on: ubuntu-22.04
8 | steps:
9 | - name: Checkout repo
10 | uses: actions/checkout@v3
11 |
12 | - name: Setup Node
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version-file: .node-version
16 | - name: Install dependencies
17 | uses: bahmutov/npm-install@v1
18 |
19 | - name: Build
20 | run: yarn build --prod
21 | - name: Upload build
22 | uses: actions/upload-artifact@v4
23 | with:
24 | name: dist
25 | path: dist/
26 |
--------------------------------------------------------------------------------
/src/components/footer/DocumentationModal.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Modal, { ModalProps } from 'react-bootstrap/Modal';
3 |
4 | import { useLocalization } from '../../util/localization';
5 |
6 | const DocumentationModal = (props: ModalProps): React.ReactElement => {
7 | const { t } = useLocalization();
8 |
9 | return (
10 |
11 |
12 | {t('Apertium_Documentation')}
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default DocumentationModal;
20 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 |
4 | import App from './App';
5 |
6 | // `BrowserRouter` uses the `history` library underneath which persists
7 | // `window.location.search` inside the hash rather than as proper query
8 | // parameters. While this behavior isn't a problem per se, it conflicts with the
9 | // pre-v5 URL scheme. For backwards compatibility, we convert forwards here.
10 | const { search } = window.location;
11 | if (search.length) {
12 | window.history.pushState({}, '', `${window.location.pathname}${window.location.hash || '#'}${search}`);
13 | }
14 |
15 | ReactDOM.render( , document.getElementById('react-mount'));
16 |
--------------------------------------------------------------------------------
/src/components/footer/ContactModal.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Modal, { ModalProps } from 'react-bootstrap/Modal';
3 |
4 | import { useLocalization } from '../../util/localization';
5 |
6 | const ContactModal = (props: ModalProps): React.ReactElement => {
7 | const { t } = useLocalization();
8 |
9 | return (
10 |
11 |
12 | {t('Contact')}
13 |
14 |
15 | {t('Contact_Us')}
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default ContactModal;
23 |
--------------------------------------------------------------------------------
/src/util/__tests__/url.test.ts:
--------------------------------------------------------------------------------
1 | import { buildNewSearch, getUrlParam } from '../url';
2 |
3 | describe('getUrlParam', () => {
4 | afterEach(() => jest.restoreAllMocks());
5 |
6 | it.each([
7 | ['dir', 'eng-spa', '?dir=eng-spa'],
8 | ['dir', 'eng-spa', '?dir=eng-spa&dir=cat-spa'],
9 | ['lang', null, '?dir=eng-spa'],
10 | ['dir', null, ''],
11 | ])('extracts %s to %s in "%s"', (param, value, search) => {
12 | expect(getUrlParam(search, param)).toBe(value);
13 | });
14 | });
15 |
16 | describe('buildNewSearch', () => {
17 | it.each([
18 | [{}, '?'],
19 | [{ dir: 'eng-spa' }, '?dir=eng-spa'],
20 | ])('maps %s to %s', (params, url) => expect(buildNewSearch(params)).toBe(url));
21 | });
22 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { createInstance } from '@datapunt/matomo-tracker-react';
2 |
3 | export enum Mode {
4 | Translation = 'translation',
5 | Analysis = 'analysis',
6 | Generation = 'generation',
7 | Sandbox = 'sandbox',
8 | }
9 |
10 | export type Config = {
11 | defaultLocale: string;
12 | htmlUrl: string;
13 | apyURL: string;
14 | stringReplacements: Record;
15 | matomoConfig?: Parameters[0];
16 |
17 | allowedLangs?: Set;
18 | allowedVariants?: Set;
19 |
20 | defaultMode: Mode;
21 | enabledModes: Set;
22 | translationChaining: boolean;
23 |
24 | subtitle?: string;
25 | subtitleColor?: string;
26 | showMoreLanguagesLink: boolean;
27 | };
28 |
--------------------------------------------------------------------------------
/src/util/url.ts:
--------------------------------------------------------------------------------
1 | import * as queryString from 'query-string';
2 |
3 | // https://stackoverflow.com/q/417142/1266600. Preserve enough space for the
4 | // host and path.
5 | // eslint-disable-next-line no-magic-numbers
6 | export const MaxURLLength = 2048 - window.location.origin.length - 25;
7 |
8 | export const getUrlParam = (search: string, key: string): string | null => {
9 | const value = queryString.parse(search)[key];
10 | if (value == null) {
11 | return null;
12 | }
13 | if (typeof value === 'string') {
14 | return value;
15 | }
16 | return value.length > 0 ? value[0] : null;
17 | };
18 |
19 | export const buildNewSearch = (params: Record): string => {
20 | return `?${queryString.stringify(params)}`;
21 | };
22 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types';
2 |
3 | export default {
4 | transform: {
5 | '^.+\\.tsx?$': [
6 | 'esbuild-jest',
7 | {
8 | sourcemap: true,
9 | },
10 | ],
11 | },
12 | moduleNameMapper: {
13 | '\\.(css)$': 'identity-obj-proxy',
14 | '\\.(gif|png|jpg|svg)$': '/src/__mocks__/file.ts',
15 | },
16 | timers: 'modern',
17 |
18 | setupFiles: ['./src/testSetup.ts'],
19 | setupFilesAfterEnv: ['./src/testEnvSetup.ts'],
20 |
21 | collectCoverageFrom: ['src/**/*.ts', 'src/**/*.tsx'],
22 | coverageThreshold: {
23 | global: {
24 | branches: 90,
25 | functions: 95,
26 | lines: 95,
27 | statements: 95,
28 | },
29 | },
30 | } as Config.InitialOptions;
31 |
--------------------------------------------------------------------------------
/config.ts:
--------------------------------------------------------------------------------
1 | import { Config, Mode } from './src/types';
2 |
3 | export default {
4 | defaultLocale: 'eng',
5 | htmlUrl: 'https://beta.apertium.org/',
6 | apyURL: 'https://beta.apertium.org/apy',
7 |
8 | defaultMode: Mode.Translation,
9 | enabledModes: new Set([Mode.Translation, Mode.Analysis, Mode.Generation, Mode.Sandbox]),
10 | translationChaining: true,
11 |
12 | subtitle: 'Beta',
13 | subtitleColor: 'rgb(220, 41, 38)',
14 |
15 | stringReplacements: {
16 | '{{maintainer}}': "Apertium ",
17 | '{{more_languages}}': "beta.apertium.org ",
18 | },
19 | showMoreLanguagesLink: false,
20 | } as Config;
21 |
--------------------------------------------------------------------------------
/src/strings/progresstable.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | completion=`grep "completion" *.json | sort -nrk1.33,1.35 -nrk1.38,1.43`
4 |
5 | if [[ $1 == "md" ]]; then
6 | echo "$completion" | awk -F '"' 'BEGIN {print "| code | CBE* | CBC** |\n|------|------|-------|" } {sub(/\..*$/, "", $1); split($4, s, " "); print "| " $1 " | " s[1] " | " s[2] " |"} END { print "\n\\*CBE: completion by entries \n\\**CBC: completion by characters (i.e., ratio of characters to English ~source)"}'
7 | else
8 | echo "$completion" | awk -F '"' 'BEGIN {print "{| class=\"wikitable sortable\"\n|-\n!|code\n!|CBE*\n!|CBC**" } {sub(/\..*$/, "", $1); split($4, s, " "); print "|-\n|| " $1 " || " s[1] " || " s[2]} END { print "|}\n\n *CBE: completion by entries \n **CBC: completion by characters (i.e., ratio of characters to English ~source)"}'
9 | fi
10 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | it('mounts to react-mount', () => {
5 | render(
6 | <>
7 |
8 |
9 | >,
10 | );
11 |
12 | jest.isolateModules(() => void require('..'));
13 | expect(screen.getByRole('img', { name: 'Apertium Box' })).toBeDefined();
14 | });
15 |
16 | it('moves actual URL parameters into URL hash', () => {
17 | window.history.replaceState(null, '', '/?dir=cat-spa');
18 |
19 | render(
20 | <>
21 |
22 |
23 | >,
24 | );
25 |
26 | jest.isolateModules(() => void require('..'));
27 |
28 | expect(window.location.hash).toContain('dir=cat-spa');
29 | });
30 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Apertium | A free/open-source machine translation platform
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | You cannot use this website without JavaScript!
20 |
21 |
--------------------------------------------------------------------------------
/src/strings/locales.json:
--------------------------------------------------------------------------------
1 | {
2 | "ara": "العَرَبِيَّة",
3 | "arg": "aragonés",
4 | "ava": "магӏарул мацӏ",
5 | "cat": "català",
6 | "deu": "deutsch",
7 | "eng": "English",
8 | "eus": "euskara",
9 | "dan": "dansk",
10 | "fin": "suomi",
11 | "fra": "français",
12 | "frp": "arpetan",
13 | "glg": "galego",
14 | "heb": "עברית",
15 | "hin": "हिन्दी",
16 | "kaa": "qaraqalpaqsha",
17 | "kaz": "қазақша",
18 | "kir": "кыргызча",
19 | "mar": "मराठी",
20 | "nno": "nynorsk",
21 | "nob": "norsk bokmål",
22 | "oci": "occitan",
23 | "por": "português",
24 | "ron": "românǎ",
25 | "rus": "русский",
26 | "sat": "ᱥᱟᱱᱛᱟᱲᱤ",
27 | "skr": "سرائیکی",
28 | "sme": "davvisámegiella",
29 | "spa": "español",
30 | "srd": "sardu",
31 | "swe": "svenska",
32 | "szl": "ślōnskŏ mŏwa",
33 | "tat": "татарча",
34 | "tur": "Türkçe",
35 | "uig": "ئۇيغۇرچە",
36 | "ukr": "українська",
37 | "uzb": "oʻzbekcha",
38 | "zho": "汉语"
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/ErrorAlert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Alert from 'react-bootstrap/Alert';
3 | import { AxiosError } from 'axios';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 | import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
6 |
7 | const isAxiosError = (error: Error): error is AxiosError => (error as AxiosError).isAxiosError;
8 |
9 | const ErrorAlert = ({ error }: { error: Error }): React.ReactElement => {
10 | let errorText = error.toString();
11 | if (
12 | isAxiosError(error) &&
13 | error.response &&
14 | error.response['data'] &&
15 | (error.response['data'] as { explanation?: string }).explanation
16 | ) {
17 | errorText = (error.response['data'] as { explanation: string }).explanation;
18 | }
19 | return (
20 |
21 | {errorText}
22 |
23 | );
24 | };
25 |
26 | export default ErrorAlert;
27 |
--------------------------------------------------------------------------------
/src/rtl.css:
--------------------------------------------------------------------------------
1 | html[dir='rtl'] body {
2 | text-align: right !important;
3 | }
4 |
5 | html[dir='rtl'] .float-right {
6 | float: left !important;
7 | }
8 |
9 | html[dir='rtl'] .float-left {
10 | float: right !important;
11 | }
12 |
13 | html[dir='rtl'] textarea {
14 | text-align: right;
15 | }
16 |
17 | html[dir='rtl'] .ml-auto {
18 | margin-left: unset !important;
19 | margin-right: auto !important;
20 | }
21 |
22 | html[dir='rtl'] .mr-1 {
23 | margin-right: unset !important;
24 | margin-left: 0.25rem !important;
25 | }
26 |
27 | html[dir='rtl'] .mr-2 {
28 | margin-right: unset !important;
29 | margin-left: 0.5rem !important;
30 | }
31 |
32 | html[dir='rtl'] .mr-3 {
33 | margin-right: unset !important;
34 | margin-left: 1rem !important;
35 | }
36 |
37 | html[dir='rtl'] .ml-1 {
38 | margin-left: unset !important;
39 | margin-right: 0.25rem !important;
40 | }
41 |
42 | html[dir='rtl'] .ml-2 {
43 | margin-left: unset !important;
44 | margin-right: 0.5rem !important;
45 | }
46 |
47 | html[dir='rtl'] .ml-3 {
48 | margin-left: unset !important;
49 | margin-right: 1rem !important;
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/__tests__/LocaleSelector.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import { LocaleContext } from '../../context';
6 | import LocaleSelector from '../LocaleSelector';
7 |
8 | it('selects currently active locale', () => {
9 | render(
10 |
11 |
12 | ,
13 | );
14 |
15 | const selector = screen.getByRole('combobox');
16 | expect((selector as HTMLSelectElement).value).toBe('spa');
17 | });
18 |
19 | it('calls setLocale on updates', () => {
20 | const setLocale = jest.fn();
21 | render(
22 |
23 |
24 | ,
25 | );
26 |
27 | const selector = screen.getByRole('combobox');
28 | userEvent.selectOptions(selector, screen.getByRole('option', { name: 'English' }));
29 |
30 | expect(setLocale).toHaveBeenCalledTimes(1);
31 | expect(setLocale).toHaveBeenCalledWith('eng');
32 | });
33 |
--------------------------------------------------------------------------------
/src/util/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | jest.dontMock('axios');
2 |
3 | import type { apyFetch as apyFetchT } from '..';
4 | import axios from 'axios';
5 |
6 | describe('apyFetch', () => {
7 | // eslint-disable-next-line
8 | const { apyFetch }: { apyFetch: typeof apyFetchT } = require('..');
9 |
10 | it('POSTs to the provided path', async () => {
11 | const [, response] = apyFetch<{ method: string }>('https://httpbin.org/anything');
12 | expect((await response).data['method']).toBe('POST');
13 | });
14 |
15 | it('throws on non-200', async () => {
16 | const [, response] = apyFetch<{ method: string }>('https://httpbin.org/status/404');
17 | await expect(response).rejects.toThrowErrorMatchingInlineSnapshot(`"Request failed with status code 404"`);
18 | });
19 |
20 | it('allows cancelling requests', async () => {
21 | const [ref, response] = apyFetch<{ method: string }>('https://httpbin.org/delay/10');
22 | ref.cancel();
23 |
24 | expect.assertions(1);
25 | try {
26 | await response;
27 | } catch (err) {
28 | // eslint-disable-next-line
29 | expect(axios.isCancel(err)).toBeTruthy();
30 | }
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/util/__tests__/languages.test.ts:
--------------------------------------------------------------------------------
1 | import { isVariant, langDirection, parentLang, toAlpha2Code, toAlpha3Code } from '../languages';
2 |
3 | describe('toAlpha2Code', () => {
4 | it.each([
5 | ['myv', null],
6 | ['eng', 'en'],
7 | ['eng_US', 'en_US'],
8 | ['en', 'en'],
9 | [null, null],
10 | ])('maps %s to %s', (before, after) => expect(toAlpha2Code(before)).toBe(after));
11 | });
12 |
13 | describe('toAlpha3Code', () => {
14 | it.each([
15 | ['xxx', 'xxx'],
16 | ['tel', 'tel'],
17 | ['eng', 'eng'],
18 | ['en_US', 'eng_US'],
19 | ['en', 'eng'],
20 | [null, null],
21 | ])('maps %s to %s', (before, after) => expect(toAlpha3Code(before)).toBe(after));
22 | });
23 |
24 | describe('langDirection', () => {
25 | it.each([
26 | ['eng', 'ltr'],
27 | ['ara', 'rtl'],
28 | ])('maps %s to %s', (lang, direction) => expect(langDirection(lang)).toBe(direction));
29 | });
30 |
31 | describe('parentLang', () => {
32 | it.each([
33 | ['eng', 'eng'],
34 | ['eng_US', 'eng'],
35 | ])('maps %s to %s', (lang, parent) => expect(parentLang(lang)).toBe(parent));
36 | });
37 |
38 | describe('isVariant', () => {
39 | it.each([
40 | ['eng', false],
41 | ['eng_US', true],
42 | ])('maps %s to %s', (lang, variant) => expect(isVariant(lang)).toBe(variant));
43 | });
44 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "project": "./tsconfig.json"
5 | },
6 | "plugins": ["@typescript-eslint", "react-hooks", "jsx-a11y", "jest"],
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "plugin:react-hooks/recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
13 | "plugin:jest/recommended",
14 | "plugin:jsx-a11y/strict"
15 | ],
16 | "settings": {
17 | "react": {
18 | "version": "detect"
19 | }
20 | },
21 | "rules": {
22 | "eqeqeq": ["error", "smart"],
23 | "complexity": ["error", 40],
24 | "no-magic-numbers": ["error", { "ignore": [-1, 0, 1, 2, 3, 4, 10, 100, 200, 1000] }],
25 | "sort-imports": [
26 | "error",
27 | {
28 | "allowSeparatedGroups": true
29 | }
30 | ],
31 | "spaced-comment": "error",
32 | "react/jsx-sort-props": "error",
33 | "react/self-closing-comp": "error",
34 | "jsx-a11y/no-autofocus": "off"
35 | },
36 | "overrides": [
37 | {
38 | "files": ["*.test.ts", "*.test.tsx"],
39 | "rules": {
40 | "no-magic-numbers": "off",
41 | "@typescript-eslint/no-unnecessary-type-assertion": "off"
42 | }
43 | }
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Check
2 | on:
3 | - push
4 | - pull_request
5 | jobs:
6 | lint:
7 | runs-on: ubuntu-22.04
8 | steps:
9 | - name: Checkout repo
10 | uses: actions/checkout@v3
11 |
12 | - name: Setup Node
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version-file: .node-version
16 | - name: Install dependencies
17 | uses: bahmutov/npm-install@v1
18 |
19 | - name: Typecheck
20 | run: yarn tsc
21 | - name: Lint scripts
22 | run: yarn eslint
23 | - name: Lint styles
24 | run: yarn stylelint
25 | - name: Prettier
26 | run: yarn prettier
27 | test:
28 | runs-on: ubuntu-22.04
29 | steps:
30 | - name: Checkout repo
31 | uses: actions/checkout@v3
32 |
33 | - name: Setup Node
34 | uses: actions/setup-node@v3
35 | with:
36 | node-version-file: .node-version
37 | - name: Install dependencies
38 | uses: bahmutov/npm-install@v1
39 |
40 | - name: Collect test coverage
41 | run: yarn coverage --ci
42 | - name: Upload coverage
43 | uses: coverallsapp/github-action@master
44 | with:
45 | github-token: ${{ secrets.GITHUB_TOKEN }}
46 | path-to-lcov: ./coverage/lcov.info
47 |
48 | - name: Check strings
49 | run: make -C src/strings test
50 |
--------------------------------------------------------------------------------
/src/strings/README.md:
--------------------------------------------------------------------------------
1 | Localization
2 | ============
3 |
4 | This directory contains the JSON files powering Html-tools' localization as well as some helpful utilities. `locales.json` contains a reference to each JSON strings file as well as each language's endonym. Localization instructions are available on the [Apertium Wiki](https://wiki.apertium.org/wiki/Apertium-html-tools).
5 |
6 | After editing localizations, please run `make` and commit its changes.
7 |
8 |
9 | | code | CBE* | CBC** |
10 | |------|------|-------|
11 | | ukr | 100% | 101.41% |
12 | | eng | 100% | 100.00% |
13 | | arg | 99% | 110.66% |
14 | | cat | 99% | 110.61% |
15 | | sat | 99% | 107.53% |
16 | | kaa | 99% | 102.73% |
17 | | hin | 99% | 102.66% |
18 | | skr | 99% | 100.09% |
19 | | dan | 99% | 99.82% |
20 | | nob | 99% | 99.69% |
21 | | nno | 99% | 98.57% |
22 | | deu | 95% | 112.44% |
23 | | fra | 95% | 108.54% |
24 | | glg | 95% | 106.96% |
25 | | frp | 95% | 106.01% |
26 | | tat | 95% | 103.28% |
27 | | tur | 95% | 102.09% |
28 | | szl | 95% | 100.09% |
29 | | mar | 95% | 99.47% |
30 | | fin | 95% | 93.59% |
31 | | ara | 95% | 89.08% |
32 | | kir | 84% | 45.06% |
33 | | srd | 80% | 89.80% |
34 | | uzb | 79% | 90.20% |
35 | | swe | 79% | 84.74% |
36 | | rus | 79% | 80.80% |
37 | | tha | 75% | 74.23% |
38 | | spa | 70% | 26.56% |
39 | | oci | 66% | 82.29% |
40 | | uig | 66% | 77.01% |
41 | | kaz | 66% | 76.30% |
42 | | heb | 66% | 67.69% |
43 | | sme | 55% | 14.53% |
44 | | por | 48% | 12.18% |
45 | | eus | 44% | 12.97% |
46 | | ava | 42% | 12.33% |
47 | | zho | 42% | 3.83% |
48 | | ron | 39% | 8.92% |
49 |
50 | \*CBE: completion by entries
51 | \**CBC: completion by characters (i.e., ratio of characters to English ~source)
52 |
--------------------------------------------------------------------------------
/src/testSetup.ts:
--------------------------------------------------------------------------------
1 | import engStrings from './strings/eng.json';
2 |
3 | const defaultStrings: Record = {};
4 | Object.keys(engStrings).forEach((key) => (defaultStrings[key] = `${key}-Default`));
5 | // eslint-disable-next-line
6 | (defaultStrings as any)['@langNames'] = { eng: 'English-Default' };
7 | defaultStrings['Maintainer'] = '{{maintainer}}-Default';
8 |
9 | // eslint-disable-next-line
10 | (window as any).PRELOADED_STRINGS = { eng: defaultStrings };
11 |
12 | // eslint-disable-next-line
13 | (window as any).PAIRS = [
14 | { sourceLanguage: 'eng', targetLanguage: 'cat' },
15 | { sourceLanguage: 'eng', targetLanguage: 'spa' },
16 | { sourceLanguage: 'cat', targetLanguage: 'eng' },
17 | { sourceLanguage: 'cat', targetLanguage: 'spa' },
18 | { sourceLanguage: 'spa', targetLanguage: 'eng' },
19 | { sourceLanguage: 'cat_foo', targetLanguage: 'spa' },
20 | { sourceLanguage: 'pan_Guru', targetLanguage: 'hin' },
21 | { sourceLanguage: 'pan_Arab', targetLanguage: 'hin' },
22 | ];
23 |
24 | // eslint-disable-next-line
25 | (window as any).PAIR_PREFS = {
26 | ['eng-cat']: { foo: { eng: 'foo_pref' }, bar: { cat: 'bar_pref' } },
27 | };
28 |
29 | // eslint-disable-next-line
30 | (window as any).ANALYZERS = { eng: 'eng-morph', spa: 'spa-morph' };
31 |
32 | // eslint-disable-next-line
33 | (window as any).GENERATORS = { eng: 'eng-gener', spa: 'spa-gener' };
34 |
35 | process.on('unhandledRejection', (err) => {
36 | // eslint-disable-next-line jest/no-jasmine-globals
37 | fail(err);
38 | });
39 |
40 | Object.defineProperty(window, 'matchMedia', {
41 | writable: true,
42 | value: jest.fn().mockImplementation((query: string) => ({
43 | matches: true,
44 | media: query,
45 | onchange: null,
46 | addEventListener: jest.fn(),
47 | removeEventListener: jest.fn(),
48 | dispatchEvent: jest.fn(),
49 | })),
50 | });
51 |
--------------------------------------------------------------------------------
/src/util/useLocalStorage.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | // Modeled after https://usehooks.com/useLocalStorage/.
4 |
5 | const storeValue = (key: string, value: T) => {
6 | const serializedValue = JSON.stringify(value);
7 | try {
8 | window.localStorage.setItem(key, serializedValue);
9 | } catch (error) {
10 | console.warn(`Failed to set LocalStorage[${key}] = ${serializedValue}`, error);
11 | }
12 | };
13 |
14 | export default (
15 | key: string,
16 | initialValue: T | (() => T),
17 | options?: {
18 | overrideValue?: T | null;
19 | validateValue?: (value: T) => boolean;
20 | },
21 | ): [T, React.Dispatch>] => {
22 | const { overrideValue, validateValue } = options || {};
23 | const validateValueFinal = validateValue || (() => true);
24 |
25 | const [stateValue, setStateValue] = React.useState(() => {
26 | if (overrideValue && validateValueFinal(overrideValue)) {
27 | storeValue(key, overrideValue);
28 | return overrideValue;
29 | }
30 |
31 | try {
32 | const item = window.localStorage.getItem(key);
33 | if (item) {
34 | const parsedItem = JSON.parse(item) as T;
35 | if (validateValueFinal(parsedItem)) {
36 | return parsedItem;
37 | } else {
38 | console.warn(`Initial value (${item}) for LocalStorage[${key}] failed validation`);
39 | }
40 | }
41 | } catch (error) {
42 | console.warn(`Failed to parse LocalStorage[${key}]`, error);
43 | }
44 |
45 | const value = initialValue instanceof Function ? initialValue() : initialValue;
46 | storeValue(key, value);
47 | return value;
48 | });
49 |
50 | const setValue = (value: T | ((val: T) => T)) => {
51 | const finalValue = value instanceof Function ? value(stateValue) : value;
52 | setStateValue(finalValue);
53 | storeValue(key, finalValue);
54 | };
55 |
56 | return [stateValue, setValue];
57 | };
58 |
--------------------------------------------------------------------------------
/src/components/LocaleSelector.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import classNames from 'classnames';
4 | import { faGlobe } from '@fortawesome/free-solid-svg-icons';
5 | import { useMatomo } from '@datapunt/matomo-tracker-react';
6 |
7 | import { LocaleContext } from '../context';
8 | import { langDirection } from '../util/languages';
9 | import locales from '../strings/locales.json';
10 |
11 | const height = '1.5rem';
12 |
13 | const LocaleSelector = ({
14 | setLocale,
15 | inverse,
16 | className = '',
17 | }: {
18 | inverse?: boolean;
19 | className?: string;
20 | setLocale: React.Dispatch>;
21 | }): React.ReactElement => {
22 | const locale = React.useContext(LocaleContext);
23 | const { trackEvent } = useMatomo();
24 |
25 | const onChange = React.useCallback(
26 | ({ target: { value } }: React.ChangeEvent) => {
27 | trackEvent({ category: 'localization', action: 'localize', name: value });
28 | setLocale(value);
29 | },
30 | [setLocale, trackEvent],
31 | );
32 |
33 | return (
34 |
35 |
36 | {/* eslint-disable-next-line jsx-a11y/no-onchange */}
37 |
38 | {Object.entries(locales)
39 | .sort(([, a], [, b]) => {
40 | return a.toLowerCase().localeCompare(b.toLowerCase());
41 | })
42 | .map(([locale, name]) => (
43 |
44 | {name}
45 |
46 | ))}
47 |
48 |
49 | );
50 | };
51 |
52 | export default LocaleSelector;
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "apertium-html-tools",
3 | "version": "5.1.8",
4 | "author": "Sushain Cherivirala ",
5 | "license": "GPL-3.0-or-later",
6 | "scripts": {
7 | "build": "ts-node build.ts",
8 | "serve": "python3 -m http.server --directory dist",
9 | "tsc": "tsc --noEmit",
10 | "eslint": "eslint . --ext .ts,.tsx --max-warnings 0",
11 | "stylelint": "stylelint 'src/**/*.css' --max-warnings 0",
12 | "test": "jest",
13 | "prettier": "prettier . --check",
14 | "verify": "$npm_execpath run tsc && $npm_execpath run eslint && $npm_execpath stylelint && $npm_execpath run prettier && $npm_execpath run test",
15 | "coverage": "jest --silent --coverage"
16 | },
17 | "dependencies": {
18 | "@datapunt/matomo-tracker-react": "^0.5.1",
19 | "@fortawesome/fontawesome-svg-core": "^6.1.1",
20 | "@fortawesome/free-solid-svg-icons": "^6.1.1",
21 | "@fortawesome/react-fontawesome": "^0.1.14",
22 | "axios": "^0.28.0",
23 | "bootstrap": "^4.6.0",
24 | "classnames": "^2.3.1",
25 | "query-string": "^7.1.1",
26 | "react": "^17.0.2",
27 | "react-bootstrap": "^1.5.2",
28 | "react-dom": "^17.0.2",
29 | "react-router-bootstrap": "^0.25.0",
30 | "react-router-dom": "^5.2.0"
31 | },
32 | "devDependencies": {
33 | "@testing-library/dom": "^8.11.1",
34 | "@testing-library/react": "^12.1.2",
35 | "@testing-library/react-hooks": "^7.0.2",
36 | "@testing-library/user-event": "^13.1.4",
37 | "@types/jest": "^26.0.22",
38 | "@types/node": "^14.14.37",
39 | "@types/react-dom": "^17.0.3",
40 | "@types/react-router-bootstrap": "^0.24.5",
41 | "@types/react-router-dom": "^5.1.7",
42 | "@typescript-eslint/eslint-plugin": "^5.9.0",
43 | "@typescript-eslint/parser": "^5.9.0",
44 | "esbuild": "^0.14.0",
45 | "esbuild-jest": "^0.4.0",
46 | "eslint": "^8.6.0",
47 | "eslint-plugin-jest": "^25.3.4",
48 | "eslint-plugin-jsx-a11y": "^6.5.1",
49 | "eslint-plugin-react": "^7.28.0",
50 | "eslint-plugin-react-hooks": "^4.3.0",
51 | "identity-obj-proxy": "^3.0.0",
52 | "jest": "^26.6.3",
53 | "jest-mock-axios": "^4.4.0",
54 | "prettier": "^2.5.1",
55 | "stylelint": "^15.10.1",
56 | "stylelint-config-prettier": "^9.0.3",
57 | "stylelint-config-standard": "^25.0.0",
58 | "ts-node": "^10.4.0",
59 | "typescript": "^4.2.4"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/strings/ron.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "shopskasalata"
5 | ],
6 | "last-updated": "2014-01-27",
7 | "locale": [
8 | "ro",
9 | "ron"
10 | ],
11 | "completion": " 39% 8.92%",
12 | "missing": [
13 | "About",
14 | "About_Apertium",
15 | "About_Title",
16 | "Apertium_Documentation",
17 | "Apertium_Downloads",
18 | "Cancel",
19 | "Contact",
20 | "Contact_Para",
21 | "Contact_Us",
22 | "Documentation",
23 | "Documentation_Para",
24 | "Download",
25 | "Downloads_Para",
26 | "Drop_Document",
27 | "Enable_JS_Warning",
28 | "File_Too_Large",
29 | "Format_Not_Supported",
30 | "Install_Apertium",
31 | "Install_Apertium_Para",
32 | "Instant_Translation",
33 | "Maintainer",
34 | "Mark_Unknown_Words",
35 | "More_Languages",
36 | "Morphological_Analysis_Help",
37 | "Morphological_Generation_Help",
38 | "Multi_Step_Translation",
39 | "Norm_Preferences",
40 | "Not_Found_Error",
41 | "Supported_Formats",
42 | "Translate_Document",
43 | "Translate_Webpage",
44 | "Translation_Help",
45 | "What_Is_Apertium",
46 | "description"
47 | ]
48 | },
49 | "title": "Apertium | O platformă liberă de traducere automată",
50 | "tagline": "O platformă liberă de traducere automată",
51 | "Translation": "Traducere",
52 | "Translate": "Traducere",
53 | "Detect_Language": "Detectarea limbii",
54 | "detected": "detectată",
55 | "Not_Available": "Traducerea nu este încă disponibilă!",
56 | "Morphological_Analysis": "Analiză morfologică",
57 | "Analyze": "Analizare",
58 | "Morphological_Generation": "Generare morfologică",
59 | "Generate": "Generare",
60 | "Spell_Checker": "Corector ortografic",
61 | "APy_Sandbox": "Cutie cu nisip APy",
62 | "APy_Sandbox_Help": "Trimiteți solciitări arbitrare",
63 | "APy_Request": "Solicitare APy",
64 | "Request": "Solicitare",
65 | "Language": "Limbă",
66 | "Input_Text": "Text de intrare",
67 | "Notice_Mistake": "Ați observat vreo greșeală?",
68 | "Help_Improve": "Ajutați-ne să îmbunătățim Apertium!"
69 | }
70 |
--------------------------------------------------------------------------------
/src/strings/zho.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "comment": "Note to future contributors: For phrases, I may have translated them wrongly. Do double-check the translations for phrases. Sentences should be fine. I have included a couple of partial translations that may help to save time.",
4 | "authors": [
5 | "wei2912"
6 | ],
7 | "last-updated": "2014-05-11",
8 | "locale": [
9 | "zh",
10 | "zho"
11 | ],
12 | "completion": " 42% 3.83%",
13 | "missing": [
14 | "APy_Request",
15 | "APy_Sandbox",
16 | "APy_Sandbox_Help",
17 | "Apertium_Documentation",
18 | "Apertium_Downloads",
19 | "Cancel",
20 | "Contact_Para",
21 | "Documentation_Para",
22 | "Downloads_Para",
23 | "Drop_Document",
24 | "Enable_JS_Warning",
25 | "File_Too_Large",
26 | "Format_Not_Supported",
27 | "Input_Text",
28 | "Install_Apertium",
29 | "Install_Apertium_Para",
30 | "Maintainer",
31 | "Mark_Unknown_Words",
32 | "More_Languages",
33 | "Morphological_Analysis_Help",
34 | "Morphological_Generation_Help",
35 | "Multi_Step_Translation",
36 | "Norm_Preferences",
37 | "Not_Found_Error",
38 | "Supported_Formats",
39 | "Translate_Document",
40 | "Translate_Webpage",
41 | "Translation_Help",
42 | "What_Is_Apertium",
43 | "description",
44 | "tagline",
45 | "title"
46 | ]
47 | },
48 | "Translation": "翻译",
49 | "Translate": "翻译",
50 | "Detect_Language": "自动检测",
51 | "detected": "检测到的语言",
52 | "Instant_Translation": "实时翻译",
53 | "Not_Available": "翻译还没有完成!",
54 | "Morphological_Analysis": "形态分析",
55 | "Analyze": "分析",
56 | "Morphological_Generation": "形态生成",
57 | "Generate": "生成",
58 | "Spell_Checker": "拼写检查器",
59 | "Request": "请求",
60 | "Language": "语言",
61 | "Notice_Mistake": "发现了任何错误?",
62 | "Help_Improve": "帮助我们改进Apertium!",
63 | "Contact_Us": "如果您发现了任何错误,希望我们work on a specific project,或者想要为Apertium做出贡献,请通知我们。",
64 | "About_Title": "关于这个网站",
65 | "About": "关于Apertium",
66 | "Download": "下载",
67 | "Contact": "联络",
68 | "Documentation": "文档",
69 | "About_Apertium": "关于Apertium"
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/translator/translator.css:
--------------------------------------------------------------------------------
1 | .language-dropdown-button > button {
2 | border-top-left-radius: 0;
3 | border-bottom-left-radius: 0;
4 | }
5 |
6 | .language-button:disabled {
7 | color: #808080;
8 | background-color: #474949;
9 | border-color: #474949;
10 | opacity: 1;
11 | }
12 |
13 | .clear-text-button,
14 | .copy-text-button {
15 | background-color: rgb(0 0 0 / 0%);
16 | padding: 0 3px;
17 | top: 3px;
18 | }
19 |
20 | .clear-text-button:hover,
21 | .clear-text-button:focus,
22 | .copy-text-button:hover,
23 | .copy-text-button:focus {
24 | background-color: #eee;
25 | color: #000;
26 | }
27 |
28 | html[dir='ltr'] .clear-text-button,
29 | html[dir='ltr'] .copy-text-button {
30 | right: 18px;
31 | }
32 |
33 | html[dir='rtl'] .clear-text-button,
34 | html[dir='rtl'] .copy-text-button {
35 | left: 18px;
36 | }
37 |
38 | .language-name-col {
39 | float: left;
40 | padding: 0 15px;
41 | position: relative;
42 | }
43 |
44 | .language-name {
45 | display: block;
46 | padding: 0;
47 | overflow-x: hidden;
48 | position: relative;
49 | text-overflow: ellipsis;
50 | border: none;
51 | background: none;
52 | width: 100%;
53 | text-align: unset;
54 | }
55 |
56 | html[dir='ltr'] .language-name {
57 | padding-left: 0.5em;
58 | }
59 |
60 | html[dir='rtl'] .language-name {
61 | padding-right: 0.5em;
62 | text-align: right;
63 | }
64 |
65 | html[dir='rtl'] .language-button:first-child {
66 | border-radius: 0 0.2rem 0.2rem 0 !important;
67 | }
68 |
69 | html[dir='rtl'] .language-dropdown-button > button {
70 | border-radius: 0.2rem 0 0 0.2rem;
71 | }
72 |
73 | .language-name:hover:not(.text-muted) {
74 | background-color: #446e9b;
75 | color: #fff;
76 | cursor: pointer;
77 | }
78 |
79 | .variant-language-name {
80 | font-size: 0.8em;
81 | }
82 |
83 | html[dir='ltr'] .variant-language-name {
84 | padding-left: 2.5em;
85 | }
86 |
87 | html[dir='rtl'] .variant-language-name {
88 | padding-right: 2.5em;
89 | }
90 |
91 | html[dir='ltr'] .translation-modes > :not(:last-child),
92 | html[dir='rtl'] .translation-modes > :not(:first-child) {
93 | margin-right: 0.5rem;
94 | }
95 |
96 | .translated-webpage {
97 | border: 1px solid #ccc;
98 | border-radius: 4px;
99 | height: 700px;
100 | }
101 |
102 | html[dir='ltr'] .translation-text-input {
103 | padding-right: 1.85rem;
104 | }
105 |
106 | html[dir='rtl'] .translation-text-input {
107 | padding-left: 1.85rem;
108 | }
109 |
--------------------------------------------------------------------------------
/src/strings/eus.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [],
4 | "last-updated": "",
5 | "locale": [
6 | "eus"
7 | ],
8 | "completion": " 44% 12.97%",
9 | "missing": [
10 | "About",
11 | "About_Apertium",
12 | "Apertium_Documentation",
13 | "Apertium_Downloads",
14 | "Cancel",
15 | "Contact",
16 | "Contact_Para",
17 | "Documentation",
18 | "Documentation_Para",
19 | "Download",
20 | "Downloads_Para",
21 | "Drop_Document",
22 | "Enable_JS_Warning",
23 | "File_Too_Large",
24 | "Format_Not_Supported",
25 | "Install_Apertium",
26 | "Install_Apertium_Para",
27 | "Instant_Translation",
28 | "Mark_Unknown_Words",
29 | "More_Languages",
30 | "Morphological_Analysis_Help",
31 | "Morphological_Generation_Help",
32 | "Multi_Step_Translation",
33 | "Norm_Preferences",
34 | "Not_Found_Error",
35 | "Supported_Formats",
36 | "Translate_Document",
37 | "Translate_Webpage",
38 | "Translation_Help",
39 | "What_Is_Apertium",
40 | "description"
41 | ]
42 | },
43 | "title": "Apertium | Kode irekiko itzulpen automatiko plataforma librea",
44 | "tagline": "Kode irekiko itzulpen automatiko plataforma librea",
45 | "Translation": "Itzulpena",
46 | "Translate": "Itzuli",
47 | "Detect_Language": "Hizkuntza detektatu",
48 | "detected": "detektatua",
49 | "Not_Available": "Itzulpena ez dago oraindik eskuragarri!",
50 | "Morphological_Analysis": "Analisi morfologikoa",
51 | "Analyze": "Analizatu",
52 | "Morphological_Generation": "Sorkuntza morfologikoa",
53 | "Generate": "Sorkuntza",
54 | "Spell_Checker": "Zuzentzaile ortografikoa",
55 | "APy_Sandbox": "APy Sandbox",
56 | "APy_Sandbox_Help": "Eskaera arbitrarioak bidali",
57 | "APy_Request": "APy Eskaera",
58 | "Request": "Eskaera",
59 | "Language": "Hizkuntza",
60 | "Input_Text": "Sarrera testua",
61 | "Notice_Mistake": "Akats bat bilatu duzu?",
62 | "Help_Improve": "Lagun iezaiguzu Apertium hobetzen!",
63 | "Contact_Us": "Akats bat bilatzemn baduzu, proiektu bat guk lantzea nahi baduzu, edo lagundu nahi ba diguzu, kontakta gaitzazu.",
64 | "Maintainer": "Webgune hau {{maintainer}}-ek mantentzen du.",
65 | "About_Title": "Webgune honetaz"
66 | }
67 |
--------------------------------------------------------------------------------
/src/strings/ava.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "solinus"
5 | ],
6 | "last-updated": "2014-04-12",
7 | "locale": [
8 | "av",
9 | "ava"
10 | ],
11 | "completion": " 42% 12.33%",
12 | "missing": [
13 | "About",
14 | "About_Apertium",
15 | "Apertium_Documentation",
16 | "Apertium_Downloads",
17 | "Cancel",
18 | "Contact",
19 | "Contact_Para",
20 | "Documentation",
21 | "Documentation_Para",
22 | "Download",
23 | "Downloads_Para",
24 | "Drop_Document",
25 | "Enable_JS_Warning",
26 | "File_Too_Large",
27 | "Format_Not_Supported",
28 | "Install_Apertium",
29 | "Install_Apertium_Para",
30 | "Instant_Translation",
31 | "Mark_Unknown_Words",
32 | "More_Languages",
33 | "Morphological_Analysis_Help",
34 | "Morphological_Generation_Help",
35 | "Multi_Step_Translation",
36 | "Norm_Preferences",
37 | "Not_Available",
38 | "Not_Found_Error",
39 | "Supported_Formats",
40 | "Translate_Document",
41 | "Translate_Webpage",
42 | "Translation_Help",
43 | "What_Is_Apertium",
44 | "description"
45 | ]
46 | },
47 | "title": "Апертиум | Машинияб таржамалъул эркенаб/рагьараб тӏагӏел",
48 | "tagline": "Машинияб таржамалъул эркенаб/рагьараб тӏагӏел",
49 | "Translation": "Таржама",
50 | "Translate": "Таржама гьабе",
51 | "Detect_Language": "Мацӏ батӏа бахъизабе",
52 | "detected": "батӏа бахъизабун буго",
53 | "Morphological_Analysis": "Морфологияб анализ",
54 | "Analyze": "Анализ гьабе",
55 | "Morphological_Generation": "Морфологияб синтез",
56 | "Generate": "Синтез гьабе",
57 | "Spell_Checker": "Битӏунхъваялъул хал-шал",
58 | "APy_Sandbox": "APy расанизари",
59 | "APy_Sandbox_Help": "Бокьараб гьикъи битӏе",
60 | "APy_Request": "APy тӏалаб гьабе",
61 | "Request": "ТӀалаб гьабе",
62 | "Language": "Мацӏ",
63 | "Input_Text": "Жаниб бачулеб текст",
64 | "Notice_Mistake": "Гъалатӏ батанищ?",
65 | "Help_Improve": "Апертиум лъиклъизе кумек гьабе!",
66 | "Contact_Us": "Гьара-рахьи ратани, гъалатӏ батун батани яги кумек гьабизе бокьун батани, хъвай нижехе.",
67 | "Maintainer": "Гьаб сайталъул кверчӏвай ва тадбир гьабулеб буго {{maintainer}}.",
68 | "About_Title": "Гьаб сайталъул хӏакъалъулъ"
69 | }
70 |
--------------------------------------------------------------------------------
/src/strings/por.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "hbwashington",
5 | "sushain97"
6 | ],
7 | "last-updated": "2014-05-11",
8 | "locale": [
9 | "pt",
10 | "por"
11 | ],
12 | "completion": " 48% 12.18%",
13 | "missing": [
14 | "About_Apertium",
15 | "Apertium_Documentation",
16 | "Apertium_Downloads",
17 | "Cancel",
18 | "Contact",
19 | "Contact_Para",
20 | "Documentation",
21 | "Documentation_Para",
22 | "Download",
23 | "Downloads_Para",
24 | "Drop_Document",
25 | "Enable_JS_Warning",
26 | "File_Too_Large",
27 | "Format_Not_Supported",
28 | "Install_Apertium",
29 | "Install_Apertium_Para",
30 | "Mark_Unknown_Words",
31 | "More_Languages",
32 | "Morphological_Analysis_Help",
33 | "Morphological_Generation_Help",
34 | "Multi_Step_Translation",
35 | "Norm_Preferences",
36 | "Not_Found_Error",
37 | "Supported_Formats",
38 | "Translate_Document",
39 | "Translate_Webpage",
40 | "Translation_Help",
41 | "What_Is_Apertium",
42 | "description"
43 | ]
44 | },
45 | "title": "Apertium | Uma plataforma livre para a tradução automática",
46 | "tagline": "Plataforma livre para a tradução automática",
47 | "Translation": "Tradução",
48 | "Translate": "Traduzir",
49 | "Detect_Language": "Detectar língua",
50 | "detected": "detectada",
51 | "Instant_Translation": "tradução instantânea",
52 | "Not_Available": "Tradução aina não disponível!",
53 | "Morphological_Analysis": "Análise morfológica",
54 | "Analyze": "Analisar!",
55 | "Morphological_Generation": "Geração morfológica",
56 | "Generate": "Gerar",
57 | "Spell_Checker": "Corretor ortográfico",
58 | "APy_Sandbox": "Entorno de provas",
59 | "APy_Sandbox_Help": "Envie qualquer consulta",
60 | "APy_Request": "Consulta de APy",
61 | "Request": "Consulta",
62 | "Language": "Língua",
63 | "Input_Text": "Texto de entrada",
64 | "Notice_Mistake": "Encontrou um erro?",
65 | "Help_Improve": "Ajude-nos a melhorar Apertium!",
66 | "Contact_Us": "Contate-nos se encontrar um erro, pensar um projeto para nós fazermos, ou quiser ajudar.",
67 | "Maintainer": "Este website é mantido por {{maintainer}}.",
68 | "About_Title": "Sobre este Website",
69 | "About": "Sobre"
70 | }
71 |
--------------------------------------------------------------------------------
/src/util/localization.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { LocaleContext, StringsContext } from '../context';
4 | import { PRELOADED_STRINGS, Strings } from './strings';
5 | import { languages, toAlpha2Code, toAlpha3Code } from './languages';
6 | import Config from '../../config';
7 | import locales from '../strings/locales.json';
8 |
9 | export { PRELOADED_STRINGS };
10 | export type { Strings };
11 |
12 | const defaultStrings = PRELOADED_STRINGS[Config.defaultLocale];
13 |
14 | const t = (locale: string, strings: Record): ((id: string) => string) => {
15 | return (id: string) => tt(id, locale, strings);
16 | };
17 |
18 | export const tt = (id: string, locale: string, strings: Record): string => {
19 | const localeStrings = strings[locale];
20 |
21 | let translated = defaultStrings[id] || id;
22 | if (localeStrings) {
23 | const localeString = localeStrings[id];
24 | if (localeString && !localeString.startsWith('%%UNAVAILABLE%%')) {
25 | translated = localeString;
26 | }
27 | }
28 |
29 | Object.entries(Config.stringReplacements).forEach(([placeholder, replacement]) => {
30 | translated = translated.replace(placeholder, replacement);
31 | });
32 |
33 | return translated;
34 | };
35 |
36 | const tLang = (locale: string, strings: Record) => {
37 | return (id: string) => ttLang(id, locale, strings);
38 | };
39 |
40 | const ttLang = (code: string, locale: string, strings: Record): string => {
41 | const alpha2Code = toAlpha2Code(code);
42 |
43 | const localeNames = strings[locale] && strings[locale]['@langNames'];
44 | if (localeNames) {
45 | const localeName = localeNames[code] || (alpha2Code && localeNames[alpha2Code]);
46 | if (localeName) {
47 | return localeName;
48 | }
49 | }
50 |
51 | const defaultNames = defaultStrings['@langNames'];
52 | if (defaultNames) {
53 | const defaultName = defaultNames[code] || (alpha2Code && defaultNames[alpha2Code]);
54 | if (defaultName) {
55 | return defaultName;
56 | }
57 | }
58 |
59 | return (alpha2Code && languages[alpha2Code]) || code;
60 | };
61 |
62 | export const useLocalization = (): { t: (id: string) => string; tLang: (code: string) => string } => {
63 | const strings = React.useContext(StringsContext);
64 | const locale = React.useContext(LocaleContext);
65 |
66 | return React.useMemo(() => ({ t: t(locale, strings), tLang: tLang(locale, strings) }), [strings, locale]);
67 | };
68 |
69 | export const validLocale = (code: string): boolean => {
70 | const alpha3Code = toAlpha3Code(code);
71 | return alpha3Code != null && alpha3Code in locales;
72 | };
73 |
--------------------------------------------------------------------------------
/src/strings/sme.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Lene Antonsen",
5 | "unhammer"
6 | ],
7 | "last-updated": "2014-05-18",
8 | "locale": [
9 | "se",
10 | "sme"
11 | ],
12 | "completion": " 55% 14.53%",
13 | "missing": [
14 | "About_Apertium",
15 | "Apertium_Documentation",
16 | "Apertium_Downloads",
17 | "Contact_Para",
18 | "Documentation_Para",
19 | "Downloads_Para",
20 | "Drop_Document",
21 | "Enable_JS_Warning",
22 | "File_Too_Large",
23 | "Format_Not_Supported",
24 | "Install_Apertium",
25 | "Install_Apertium_Para",
26 | "Instant_Translation",
27 | "Mark_Unknown_Words",
28 | "More_Languages",
29 | "Morphological_Analysis_Help",
30 | "Morphological_Generation_Help",
31 | "Multi_Step_Translation",
32 | "Norm_Preferences",
33 | "Not_Found_Error",
34 | "Supported_Formats",
35 | "Translate_Document",
36 | "Translation_Help",
37 | "What_Is_Apertium",
38 | "description"
39 | ]
40 | },
41 | "title": "Apertium | Friddja ja rabasgáldokodavuđot dihtorjorgalanvuogádat",
42 | "tagline": "Friddja ja rabasgáldokodavuđot dihtorjorgalanvuogádat",
43 | "Translation": "Jorgaleapmi",
44 | "Translate": "Jorgal",
45 | "Detect_Language": "Dovdá giela",
46 | "detected": "Árvaluvvon giella",
47 | "Not_Available": "Jorgalus ii gávdno!",
48 | "Cancel": "Gaskkalduhte",
49 | "Translate_Webpage": "Jorgal neahttasiiddu",
50 | "Morphological_Analysis": "Morfologalaš analysa",
51 | "Analyze": "Analysere",
52 | "Morphological_Generation": "Morfologalaš genereren",
53 | "Generate": "Generere",
54 | "Spell_Checker": "Sátnedárkkisteapmi",
55 | "APy_Sandbox": "APy-sáttokássa",
56 | "APy_Sandbox_Help": "Gohččomiid geavaheapmi",
57 | "APy_Request": "APy-gohččun",
58 | "Request": "Gohččun",
59 | "Language": "Giella",
60 | "Input_Text": "Álgoteaksta",
61 | "Notice_Mistake": "Gávdnetgo meattáhusa?",
62 | "Help_Improve": "Veahket min buoridit Apertiuma!",
63 | "Contact_Us": "Váldde áinnas oktavuođa minguin jus gávnnat meattáhusa, jus dus lea miella bargat prošeavttain dahje don eará ládje háliidat min veahkehit.",
64 | "Maintainer": "Dán neahttabáikki fuolaha {{maintainer}}.",
65 | "About_Title": "Neahttabáikki birra",
66 | "About": "Birra",
67 | "Download": "Viežžat",
68 | "Contact": "Váldde oktavuođa",
69 | "Documentation": "Dokumentašuvdna"
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/__tests__/WithLocale.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cleanup, render, screen, waitFor } from '@testing-library/react';
3 | import mockAxios from 'jest-mock-axios';
4 |
5 | import Config from '../../../config';
6 | import { LocaleContext } from '../../context';
7 | import WithLocale from '../WithLocale';
8 |
9 | const setLocation = (location: string) => window.history.replaceState({}, '', location);
10 |
11 | afterEach(() => setLocation('/'));
12 |
13 | const ShowLocale: React.FunctionComponent<{ setLocale: React.Dispatch> }> = () => (
14 | {React.useContext(LocaleContext)}
15 | );
16 |
17 | const renderWithLocale = () => render({(props) => } );
18 |
19 | describe('default locale selection', () => {
20 | it('restores from browser state', () => {
21 | setLocation('/index.heb.html');
22 | renderWithLocale();
23 |
24 | cleanup();
25 |
26 | setLocation('/');
27 | renderWithLocale();
28 |
29 | expect(screen.getByRole('main').textContent).toEqual('heb');
30 | });
31 |
32 | it('prefers lang parameter over url path', () => {
33 | setLocation('/index.heb.html?lang=spa');
34 | renderWithLocale();
35 | expect(screen.getByRole('main').textContent).toEqual('spa');
36 | });
37 |
38 | it('prefers url path over browser storage', () => {
39 | setLocation('/index.heb.html');
40 | renderWithLocale();
41 |
42 | cleanup();
43 |
44 | setLocation('/index.spa.html');
45 | renderWithLocale();
46 | expect(screen.getByRole('main').textContent).toEqual('spa');
47 | });
48 |
49 | describe('fetching locale from APy', () => {
50 | it('uses valid response', async () => {
51 | renderWithLocale();
52 |
53 | mockAxios.mockResponse({ data: ['es'] });
54 | await waitFor(() =>
55 | expect(mockAxios.post).toHaveBeenCalledWith(expect.stringContaining('getLocale'), '', expect.anything()),
56 | );
57 |
58 | expect(screen.getByRole('main').textContent).toEqual('spa');
59 | });
60 |
61 | it('handles variants', () => {
62 | renderWithLocale();
63 |
64 | mockAxios.mockResponse({ data: ['en-US'] });
65 |
66 | expect(screen.getByRole('main').textContent).toEqual('eng');
67 | });
68 |
69 | it('falls back to default for unknown response', () => {
70 | renderWithLocale();
71 |
72 | mockAxios.mockResponse({ data: ['zzz'] });
73 |
74 | expect(screen.getByRole('main').textContent).toEqual(Config.defaultLocale);
75 | });
76 |
77 | it('falls back to default on failure', () => {
78 | renderWithLocale();
79 |
80 | mockAxios.mockError();
81 |
82 | expect(screen.getByRole('main').textContent).toEqual(Config.defaultLocale);
83 | });
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/src/components/WithLocale.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { ConfigContext, LocaleContext } from '../context';
4 | import { apyFetch } from '../util';
5 | import { getUrlParam } from '../util/url';
6 | import { toAlpha3Code } from '../util/languages';
7 | import useLocalStorage from '../util/useLocalStorage';
8 | import { validLocale } from '../util/localization';
9 |
10 | const loadBrowserLocale = (apyURL: string, setLocale: React.Dispatch>) => {
11 | void (async () => {
12 | let locales: Array;
13 | try {
14 | locales = (await apyFetch(`${apyURL}/getLocale`)[1]).data as Array;
15 | } catch (error) {
16 | console.warn('Failed to fetch browser locale, falling back to default', error);
17 | return;
18 | }
19 |
20 | for (let localeGuess of locales) {
21 | if (localeGuess.indexOf('-') !== -1) {
22 | localeGuess = localeGuess.split('-')[0];
23 | }
24 |
25 | const locale = toAlpha3Code(localeGuess);
26 | if (validLocale(locale)) {
27 | setLocale(locale);
28 | }
29 | }
30 | })();
31 | };
32 |
33 | const WithLocale = ({
34 | children,
35 | }: {
36 | children: (props: { setLocale: React.Dispatch> }) => React.ReactNode;
37 | }): React.ReactElement => {
38 | const { apyURL, defaultLocale } = React.useContext(ConfigContext);
39 |
40 | // Locale selection priority:
41 | // 1. `lang` parameter from URL
42 | // 2. locale section from URL path
43 | // 3. `locale` key from LocalStorage
44 | // 4. browser's preferred locale from APy
45 | const urlPathMatch = /index\.(\w{3})\.html/.exec(window.location.pathname);
46 | const urlPathLocale = urlPathMatch && urlPathMatch[1];
47 | const langParam = getUrlParam(window.location.search, 'lang');
48 | const urlQueryLocale = toAlpha3Code(langParam)?.replace('/', '');
49 | let shouldLoadBrowserLocale = false;
50 | const [locale, setLocale] = useLocalStorage(
51 | 'locale',
52 | () => {
53 | shouldLoadBrowserLocale = true;
54 | return defaultLocale;
55 | },
56 | { overrideValue: urlQueryLocale || urlPathLocale, validateValue: validLocale },
57 | );
58 | React.useEffect(() => {
59 | if (shouldLoadBrowserLocale) {
60 | loadBrowserLocale(apyURL, setLocale);
61 | }
62 | }, [shouldLoadBrowserLocale, setLocale, apyURL]);
63 |
64 | React.useEffect(() => {
65 | // We use the real `window.history` here since we intend to modify the real
66 | // URL path, not just the URL hash.
67 | window.history.pushState({}, '', `index.${locale}.html${window.location.search}${window.location.hash}`);
68 | }, [locale]);
69 |
70 | return {children({ setLocale })} ;
71 | };
72 |
73 | export default WithLocale;
74 |
--------------------------------------------------------------------------------
/src/util/__tests__/localization.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { renderHook } from '@testing-library/react-hooks';
3 |
4 | import { LocaleContext, StringsContext } from '../../context';
5 | import { Strings, useLocalization, validLocale } from '../localization';
6 |
7 | describe('validLocale', () => {
8 | it.each([
9 | ['eng', true],
10 | ['en', true],
11 | ['tel', false],
12 | ])('maps %s to %s', (lang, valid) => expect(validLocale(lang)).toBe(valid));
13 | });
14 |
15 | describe('useLocalization', () => {
16 | describe('with loaded locale', () => {
17 | const wrapper = ({ children }: { children: React.ReactElement[] }) => (
18 |
27 | {children}
28 |
29 | );
30 | const {
31 | result: {
32 | error,
33 | current: { t, tLang },
34 | },
35 | } = renderHook(() => useLocalization(), { wrapper });
36 |
37 | it('does not error', () => expect(error).toBeUndefined());
38 |
39 | describe('t', () => {
40 | it.each([
41 | ['Input_Text', 'Input_Text-Default'], // default
42 | ['Translation', 'Traducción'], // present
43 | ['Detect_Language', 'Detect_Language-Default'], // unavailable
44 | ['SomethingMissing', 'SomethingMissing'], // missing
45 |
46 | // replacements
47 | [
48 | 'Maintainer',
49 | `Apertium -Default`,
50 | ],
51 | ])('maps %s to %s', (id, value) => expect(t(id)).toBe(value));
52 | });
53 |
54 | describe('tLang', () => {
55 | it.each([
56 | ['eng', 'English-Default'], // default
57 | ['sco', 'escocés'], // present
58 | ['fin', 'suomi'], // autonym
59 | ['xyz', 'xyz'], // missing
60 | ])('maps %s to %s', (code, name) => expect(tLang(code)).toBe(name));
61 | });
62 | });
63 |
64 | describe('without loaded locale', () => {
65 | const wrapper = ({ children }: { children: React.ReactElement[] }) => (
66 |
67 | {children}
68 |
69 | );
70 | const {
71 | result: {
72 | error,
73 | current: { t, tLang },
74 | },
75 | } = renderHook(() => useLocalization(), { wrapper });
76 |
77 | it('does not error', () => expect(error).toBeUndefined());
78 |
79 | it('t returns defaults', () => expect(t('Input_Text')).toBe('Input_Text-Default'));
80 |
81 | it('tLang returns defaults', () => expect(tLang('eng')).toBe('English-Default'));
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/src/components/translator/index.ts:
--------------------------------------------------------------------------------
1 | import { parentLang } from '../../util/languages';
2 |
3 | export type Pairs = Readonly>>;
4 | export type NamedLangs = Array<[string, string]>;
5 |
6 | // eslint-disable-next-line
7 | const rawPairs = (window as any).PAIRS as Array<{
8 | sourceLanguage: string;
9 | targetLanguage: string;
10 | }>;
11 | export const DirectPairs: Pairs = rawPairs.reduce((pairs, { sourceLanguage, targetLanguage }) => {
12 | pairs[sourceLanguage] = pairs[sourceLanguage] || new Set();
13 | pairs[sourceLanguage].add(targetLanguage);
14 |
15 | const parent = parentLang(sourceLanguage);
16 | pairs[parent] = pairs[parent] || new Set();
17 |
18 | return pairs;
19 | }, {} as Record>);
20 |
21 | const getChainedTgtLangs = (srcLang: string) => {
22 | const tgtLangs: Set = new Set();
23 |
24 | const tgtsSeen = new Set([srcLang]);
25 | let tgsFrontier = [...DirectPairs[srcLang]];
26 | let tgtLang;
27 | while ((tgtLang = tgsFrontier.pop())) {
28 | if (!tgtsSeen.has(tgtLang)) {
29 | tgtLangs.add(tgtLang);
30 | if (DirectPairs[tgtLang]) {
31 | tgsFrontier = [...tgsFrontier, ...DirectPairs[tgtLang]];
32 | }
33 | tgtsSeen.add(tgtLang);
34 | }
35 | }
36 |
37 | return tgtLangs;
38 | };
39 | export const chainedPairs: Record> = {};
40 | Object.keys(DirectPairs).forEach((srcLang) => {
41 | chainedPairs[srcLang] = getChainedTgtLangs(srcLang);
42 | });
43 | export const ChainedPairs: Pairs = chainedPairs;
44 |
45 | export const SrcLangs = new Set(Object.keys(DirectPairs));
46 |
47 | export const TgtLangs = new Set(
48 | ([] as Array).concat(...Object.values(DirectPairs).map((ls) => Array.from(ls))),
49 | );
50 | TgtLangs.forEach((lang) => {
51 | const parent = parentLang(lang);
52 | if (!TgtLangs.has(parent)) {
53 | TgtLangs.add(parent);
54 | }
55 | });
56 |
57 | export const isPair = (pairs: Pairs, src: string, tgt: string): boolean => pairs[src] && pairs[src].has(tgt);
58 |
59 | // eslint-disable-next-line
60 | const pairPrefs = (window as any).PAIR_PREFS as Record>>;
61 |
62 | export type PairPrefs = Record;
63 | export type PairPrefValues = Record;
64 |
65 | export const getPairPrefs = (locale: string, srcLang: string, tgtLang: string): PairPrefs => {
66 | const localizedPrefs: Record = {};
67 | Object.entries(pairPrefs[`${srcLang}-${tgtLang}`] || {}).forEach(([id, prefs]) => {
68 | localizedPrefs[id] = prefs[locale] || Object.values(prefs)[0];
69 | });
70 | return localizedPrefs;
71 | };
72 |
73 | export enum Mode {
74 | Text,
75 | Document,
76 | Webpage,
77 | }
78 |
79 | export const pairUrlParam = 'dir';
80 |
81 | export const baseUrlParams = ({ srcLang, tgtLang }: { srcLang: string; tgtLang: string }): Record => {
82 | const pair = `${srcLang}-${tgtLang}`;
83 | return { [pairUrlParam]: pair };
84 | };
85 |
86 | export const TranslateEvent = 'translate';
87 |
88 | export const DetectEvent = 'detect-language';
89 | export const DetectCompleteEvent = 'detect-language-complete';
90 |
--------------------------------------------------------------------------------
/src/components/__tests__/Sandbox.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cleanup, render, screen, waitFor } from '@testing-library/react';
3 | import { Router } from 'react-router-dom';
4 | import { createMemoryHistory } from 'history';
5 | import mockAxios from 'jest-mock-axios';
6 | import userEvent from '@testing-library/user-event';
7 |
8 | import Sandbox from '../Sandbox';
9 |
10 | const input = '/analyse?lang=eng&q=kicked';
11 |
12 | const renderSandbox = () =>
13 | render(
14 |
15 |
16 | ,
17 | );
18 |
19 | const type = (input: string): HTMLTextAreaElement => {
20 | const textbox = screen.getByRole('textbox');
21 | userEvent.type(textbox, input);
22 | return textbox as HTMLTextAreaElement;
23 | };
24 |
25 | const submit = () => userEvent.click(screen.getByRole('button'));
26 |
27 | it('allows typing an input', () => {
28 | renderSandbox();
29 |
30 | const textbox = type(input);
31 |
32 | expect(textbox.value).toBe(input);
33 | });
34 |
35 | it('persists input in browser storage', () => {
36 | renderSandbox();
37 | type(input);
38 | cleanup();
39 |
40 | renderSandbox();
41 |
42 | const textbox = screen.getByRole('textbox');
43 | expect((textbox as HTMLSelectElement).value).toBe(input);
44 | });
45 |
46 | describe('requests', () => {
47 | it('no-ops an empty input', () => {
48 | renderSandbox();
49 | submit();
50 | expect(mockAxios.post).not.toBeCalled();
51 | });
52 |
53 | it('requests on button click', async () => {
54 | renderSandbox();
55 | type(input);
56 | submit();
57 |
58 | mockAxios.mockResponse({
59 | data: [
60 | ['kicked/kick/kick', 'kicked'],
61 | ["'/'/'s", "'"],
62 | ],
63 | });
64 | await waitFor(() =>
65 | expect(mockAxios.post).toHaveBeenCalledWith(expect.stringContaining(input), '', expect.anything()),
66 | );
67 |
68 | const output = screen.getByRole('main');
69 | expect(output.textContent).toContain('ms');
70 | expect(output.textContent).toContain('"kicked/kick/kick",');
71 | });
72 |
73 | it('requests on enter', async () => {
74 | renderSandbox();
75 |
76 | type(`${input}{enter}`);
77 |
78 | mockAxios.mockResponse({ data: 'the-response' });
79 | await waitFor(() => expect(mockAxios.post).toHaveBeenCalledTimes(1));
80 |
81 | const output = screen.getByRole('main');
82 | expect(output.textContent).toContain('the-response');
83 | });
84 |
85 | it('shows errors', async () => {
86 | renderSandbox();
87 | type(input);
88 | submit();
89 |
90 | mockAxios.mockError({
91 | response: {
92 | data: { status: 'error', code: 400, message: 'Bad Request', explanation: 'That mode is not installed' },
93 | },
94 | });
95 | await waitFor(() => expect(mockAxios.post).toHaveBeenCalledTimes(1));
96 |
97 | const error = screen.getByRole('alert');
98 | expect(error.textContent).toMatchInlineSnapshot(`" That mode is not installed"`);
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/src/strings/spa.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "spectre360",
5 | "hbwashington",
6 | "sushain97"
7 | ],
8 | "last-updated": "2014-05-11",
9 | "locale": [
10 | "es",
11 | "spa"
12 | ],
13 | "completion": " 70% 26.56%",
14 | "missing": [
15 | "Apertium_Documentation",
16 | "Contact_Para",
17 | "Documentation_Para",
18 | "Downloads_Para",
19 | "Drop_Document",
20 | "Install_Apertium",
21 | "Install_Apertium_Para",
22 | "More_Languages",
23 | "Morphological_Analysis_Help",
24 | "Morphological_Generation_Help",
25 | "Multi_Step_Translation",
26 | "Norm_Preferences",
27 | "Supported_Formats",
28 | "Translate_Webpage",
29 | "Translation_Help",
30 | "What_Is_Apertium",
31 | "description"
32 | ]
33 | },
34 | "title": "Apertium | Una plataforma libre para la traducción automática",
35 | "tagline": "Plataforma libre para la traducción automática",
36 | "Translation": "Traducción",
37 | "Translate": "Traducir",
38 | "Detect_Language": "Detectar idioma",
39 | "detected": "detectado",
40 | "Instant_Translation": "Traducción instantánea",
41 | "Mark_Unknown_Words": "Marcar palabras desconocidas",
42 | "Translate_Document": "Traducir un documento",
43 | "Not_Available": "Traducción todavía no disponible!",
44 | "File_Too_Large": "¡El fichero es demasiado grande!",
45 | "Cancel": "Cancelar",
46 | "Format_Not_Supported": "Formato desconocido!",
47 | "Morphological_Analysis": "Análisis morfológico",
48 | "Analyze": "Analizar",
49 | "Morphological_Generation": "Generación morfológica",
50 | "Generate": "Generar",
51 | "Spell_Checker": "Corrector ortográfico",
52 | "APy_Sandbox": "Entorno de pruebas",
53 | "APy_Sandbox_Help": "Envia cualquier consulta",
54 | "APy_Request": "Consulta de APy",
55 | "Request": "Consulta",
56 | "Language": "Idioma",
57 | "Input_Text": "Texto de entrada",
58 | "Notice_Mistake": "¿Has encontrado un error?",
59 | "Help_Improve": "Ayúdanos a mejorar Apertium!",
60 | "Contact_Us": "Por favor ponte en contacto con nosotros si encuentras un error en la página, si hay un proyecto que crees que nos podría interesar o si te interesaría colaborar con nosotros.",
61 | "Maintainer": "Esta página está gestionada por {{maintainer}}.",
62 | "About_Title": "Sobre Apertium",
63 | "Enable_JS_Warning": "La web sólo funciona si tienes habilitado JavaScript, si no puedes habilitar JavaSCript , puedes utilizar los traductores de Apertium a Prompsit .",
64 | "Not_Found_Error": "404 Error: Lo sentimos, ¡esta página ya no existe!",
65 | "About": "Acerca de",
66 | "Download": "Descargas",
67 | "Contact": "Contactar",
68 | "Documentation": "Documentación",
69 | "About_Apertium": "Acerca de Apertium",
70 | "Apertium_Downloads": "Descargas de Apertium"
71 | }
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Apertium Html-tools
2 |
3 | [](https://github.com/apertium/apertium-html-tools/actions/workflows/check.yml?query=branch%3Amaster)
5 | [](https://coveralls.io/github/apertium/apertium-html-tools?branch=master)
6 |
7 | [Apertium Html-tools][1] is a web application providing a fully localised
8 | interface for text/document/website translation, analysis, and generation
9 | powered by [Apertium][2]. Html-tools relies on an Apertium HTTP API such as
10 | [Apertium-apy][3] or [ScaleMT][4] (to a lesser extent). More information along
11 | with instructions for localization is available on the [Apertium Wiki][5].
12 |
13 | ## Configuration
14 |
15 | Configure the build by editing `config.ts`.
16 |
17 | ## Dependencies
18 |
19 | ### Development
20 |
21 | Our sources are written in [TypeScript][6].
22 |
23 | Development requires installing [Node.js][7] and [Yarn][8]. After installing
24 | both, use `yarn install --dev` to install JavaScript packages. We use
25 | [ESLint][9] & [Stylelint][10] for linting, [Prettier][11] for code formatting
26 | and [Jest][12] as a test runner.
27 |
28 | ### Runtime
29 |
30 | We use a variety of JS libraries at runtime:
31 |
32 | - [React](https://reactjs.org/)
33 | - [React-Bootstrap](https://react-bootstrap.netlify.app/)
34 | - [Font Awesome](https://fontawesome.com/)
35 | - [React Router](https://reactrouter.com/)
36 |
37 | To avoid distributing hundreds of JS files, we use [esbuild][13] to bundle
38 | sources into browser-ready JS.
39 |
40 | ## Building
41 |
42 | First, follow the development instructions. Then, running `yarn build` will
43 | output built bundles to `dist/`. Use `--prod` to minify bundles. Any web server
44 | capable of serving static assets can be pointed directly to `dist/`.
45 |
46 | Alternatively, if you'd like to avoid polluting your host system with build
47 | dependencies, use Docker:
48 |
49 | docker build -t apertium-html-tools .
50 | docker run --rm -v $(pwd)/dist:/root/dist apertium-html-tools
51 |
52 | ## Contributing
53 |
54 | - Use `yarn build --watch` to keep `dist/` up-to-date with new bundles.
55 | - Use `yarn serve` to run a simple Python server which serves `dist/` on
56 | `localhost:8000`.
57 | - Use `yarn verify` to run the typechecker, linters and tests. See
58 | `package.json` for more granular scripts.
59 |
60 | To analyze the bundle size, run a prod build and upload the resulting
61 | `meta.json` file to [Bundle Buddy][14].
62 |
63 | We use [GitHub Actions][15] to run tests, linting, typechecking, etc. on each
64 | commit.
65 |
66 | [1]: https://wiki.apertium.org/wiki/Apertium-html-tools
67 | [2]: https://apertium.org
68 | [3]: https://wiki.apertium.org/wiki/Apertium-apy
69 | [4]: https://wiki.apertium.org/wiki/ScaleMT
70 | [5]: https://wiki.apertium.org/wiki/Apertium-html-tools
71 | [6]: https://www.typescriptlang.org/
72 | [7]: https://nodejs.org/en/download/
73 | [8]: https://classic.yarnpkg.com/en/docs/install
74 | [9]: https://eslint.org/
75 | [10]: https://stylelint.io/
76 | [11]: https://prettier.io/
77 | [12]: https://jestjs.io/
78 | [13]: https://esbuild.github.io/
79 | [14]: https://bundle-buddy.com/
80 | [15]: https://docs.github.com/actions
81 |
--------------------------------------------------------------------------------
/src/components/translator/__tests__/WithSortedLanguages.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@testing-library/react';
3 |
4 | import WithSortedLanguages, { ChildProps, Props } from '../WithSortedLanguages';
5 |
6 | const getSortedLangs = (props: Omit): [Array, Array] => {
7 | const childProps: { props: ChildProps } = { props: {} as ChildProps };
8 |
9 | render(
10 |
11 | {(props: ChildProps) => {
12 | childProps.props = props;
13 | return
;
14 | }}
15 | ,
16 | );
17 |
18 | return [childProps.props.srcLangs.map(([code]) => code), childProps.props.tgtLangs.map(([code]) => code)];
19 | };
20 |
21 | // Adapted from https://stackoverflow.com/a/37580979/1266600.
22 | function* permute(permutation_: Array): Generator> {
23 | const permutation = permutation_.slice(),
24 | length = permutation.length,
25 | c = Array(length).fill(0) as Array;
26 |
27 | let i = 1,
28 | k: number,
29 | p: T;
30 |
31 | yield permutation.slice();
32 | while (i < length) {
33 | if (c[i] < i) {
34 | k = i % 2 && c[i];
35 | p = permutation[i];
36 | permutation[i] = permutation[k];
37 | permutation[k] = p;
38 | ++c[i];
39 | i = 1;
40 | yield permutation.slice();
41 | } else {
42 | c[i] = 0;
43 | ++i;
44 | }
45 | }
46 | }
47 |
48 | describe('source language sorting', () => {
49 | it.each([
50 | // language name
51 | [['cat', 'eng', 'spa']],
52 |
53 | // variants after parent
54 | [['cat', 'cat_bar', 'eng', 'eng_foo', 'spa']],
55 |
56 | // variants by name
57 | [['cat', 'cat_bar', 'cat_foo', 'eng', 'spa']],
58 | ])('sorts to %s', (srcLangs) => {
59 | for (const srcLangsPermutation of permute(srcLangs)) {
60 | const [sortedSrcLangs] = getSortedLangs({
61 | pairs: {},
62 | srcLang: 'eng',
63 | srcLangs: new Set(srcLangsPermutation),
64 | tgtLangs: new Set(),
65 | });
66 | expect(sortedSrcLangs).toStrictEqual(srcLangs);
67 | }
68 | });
69 | });
70 |
71 | describe('target language sorting', () => {
72 | const srcLang = 'eng';
73 |
74 | it.each([
75 | // possible languages first
76 | {
77 | possibleTgtLangs: ['spa'],
78 | tgtLangs: ['spa', 'cat', srcLang],
79 | },
80 |
81 | // possible families first
82 | {
83 | possibleTgtLangs: ['spa_foo'],
84 | tgtLangs: ['spa', 'spa_foo', 'cat', srcLang],
85 | },
86 |
87 | // possible variants first
88 | {
89 | possibleTgtLangs: ['spa_foo'],
90 | tgtLangs: ['spa', 'spa_foo', 'spa_bar', 'cat', srcLang],
91 | },
92 |
93 | // possible variants by name
94 | {
95 | possibleTgtLangs: ['spa_foo', 'spa_bar'],
96 | tgtLangs: ['spa', 'spa_bar', 'spa_foo', 'cat', srcLang],
97 | },
98 | ])('sorts to $tgtLangs when $possibleTgtLangs possible', ({ tgtLangs, possibleTgtLangs }) => {
99 | for (const tgtLangsPermutation of permute(tgtLangs)) {
100 | const [, sortedTgtLangs] = getSortedLangs({
101 | srcLang,
102 | srcLangs: new Set(),
103 | tgtLangs: new Set(tgtLangsPermutation),
104 | pairs: { [srcLang]: new Set(possibleTgtLangs) },
105 | });
106 | expect(sortedTgtLangs).toStrictEqual(tgtLangs);
107 | }
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/src/components/__tests__/WithInstallationAlert.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { act, render, screen, waitFor } from '@testing-library/react';
3 | import mockAxios from 'jest-mock-axios';
4 | import userEvent from '@testing-library/user-event';
5 |
6 | import { APyContext } from '../../context';
7 | import WithInstallationAlert from '../WithInstallationAlert';
8 |
9 | const renderWithInstallationAlert = (length = 1) => {
10 | const TestComponent = () => {
11 | const apyFetch = React.useContext(APyContext);
12 | Array.from({ length }, () => apyFetch(''));
13 | return null;
14 | };
15 |
16 | render(
17 |
18 |
19 | ,
20 | );
21 | };
22 |
23 | afterEach(() => jest.setSystemTime(jest.getRealSystemTime()));
24 |
25 | it('is closed by default', () => {
26 | render( );
27 | expect(screen.queryByRole('alert')).toBeNull();
28 | });
29 |
30 | it('renders children', () => {
31 | render(
32 |
33 | hello
34 | ,
35 | );
36 | expect(screen.getByRole('main').textContent).toBe('hello');
37 | });
38 |
39 | describe('request interactions', () => {
40 | it('does not open after a fast request', async () => {
41 | renderWithInstallationAlert();
42 |
43 | await waitFor(() => expect(mockAxios.queue()).toHaveLength(1));
44 | jest.setSystemTime(Date.now() + 100);
45 | act(() => mockAxios.mockResponse());
46 |
47 | expect(screen.queryByRole('alert')).toBeNull();
48 | });
49 |
50 | it('opens due to a single slow request', async () => {
51 | renderWithInstallationAlert();
52 |
53 | await waitFor(() => expect(mockAxios.queue()).toHaveLength(1));
54 | jest.setSystemTime(Date.now() + 100000);
55 | act(() => mockAxios.mockResponse());
56 |
57 | expect(screen.getByRole('alert').textContent).toContain('Install_Apertium-Default');
58 | });
59 |
60 | it('opens after a series of slow requests', async () => {
61 | renderWithInstallationAlert(6);
62 |
63 | await waitFor(() => expect(mockAxios.queue()).toHaveLength(6));
64 |
65 | jest.setSystemTime(Date.now() + 3100);
66 | act(() => void Array.from({ length: 5 }, () => mockAxios.mockResponse()));
67 |
68 | expect(screen.queryByRole('alert')).toBeNull();
69 |
70 | act(() => mockAxios.mockResponse());
71 |
72 | expect(screen.getByRole('alert')).toBeDefined();
73 | });
74 | });
75 |
76 | describe('open behavior', () => {
77 | const openAlert = async () => {
78 | renderWithInstallationAlert();
79 |
80 | await waitFor(() => expect(mockAxios.queue()).toHaveLength(1));
81 | act(() => {
82 | jest.setSystemTime(Date.now() + 100000);
83 | mockAxios.mockResponse();
84 | });
85 | };
86 |
87 | it('closes after button click', async () => {
88 | await openAlert();
89 |
90 | userEvent.click(screen.getByRole('button'));
91 |
92 | await waitFor(() => expect(screen.queryByRole('alert')).toBeNull());
93 | });
94 |
95 | it('closes after timeout', async () => {
96 | await openAlert();
97 |
98 | act(() => void jest.advanceTimersByTime(110000));
99 |
100 | await waitFor(() => expect(screen.queryByRole('alert')).toBeNull());
101 | });
102 |
103 | it('stays open on hover', async () => {
104 | await openAlert();
105 |
106 | userEvent.hover(screen.getByRole('alert'));
107 | jest.runAllTimers();
108 |
109 | expect(screen.getByRole('alert')).toBeDefined();
110 |
111 | userEvent.unhover(screen.getByRole('alert'));
112 |
113 | act(() => void jest.advanceTimersByTime(110000));
114 |
115 | await waitFor(() => expect(screen.queryByRole('alert')).toBeNull());
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/src/strings/kir.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "jonorthwash",
5 | "tolgonay"
6 | ],
7 | "last-updated": "2014-05-10",
8 | "locale": [
9 | "ky",
10 | "kir"
11 | ],
12 | "completion": " 84% 45.06%",
13 | "missing": [
14 | "Apertium_Documentation",
15 | "Contact_Para",
16 | "Downloads_Para",
17 | "Install_Apertium_Para",
18 | "More_Languages",
19 | "Morphological_Generation_Help",
20 | "Multi_Step_Translation",
21 | "Norm_Preferences",
22 | "What_Is_Apertium"
23 | ]
24 | },
25 | "title": "Apertium | Эркин/ачык машинелик котормо платформасы",
26 | "tagline": "Эркин/ачык машинелик котормо платформасы",
27 | "description": "Апертиум деген эрежелүү машинелик котормо флатформасы. Apertium is a rule-based machine translation platform. Ал эркин жана ачык софт, жана GNU GPL жалпы ачык лицензиясынын шарттары боюнча камсыздалат.",
28 | "Translation": "Которуу",
29 | "Translation_Help": "Текст же URL киргизиңиз.",
30 | "Translate": "Котор",
31 | "Detect_Language": "Тилди автоматтык түрдө аныктоо",
32 | "detected": "автоматтык түрдө аныкталды",
33 | "Instant_Translation": "Ыкчам которуу",
34 | "Mark_Unknown_Words": "Билбеген сөздөрдү белгилөө",
35 | "Translate_Document": "Документ которуу",
36 | "Drop_Document": "Документ бул жерге ташыңыз",
37 | "Not_Available": "Котормо даяр эмес",
38 | "File_Too_Large": "Файлдын көлөмү лимиттен ашып кетти!",
39 | "Cancel": "Артка кайтуу",
40 | "Format_Not_Supported": "Формат колдолбойт",
41 | "Translate_Webpage": "Веб барагы которуу",
42 | "Supported_Formats": "Колдолгон форматтар: .txt, .rtf, .odp, .ods, .odt (LibreOffice/OpenOffice), .xlsx, .pptx, .docx (Microsoft Office 2003-төн кийинки). Word 97 .doc файлдары жана .pdf файлдары колдолбойт. ",
43 | "Morphological_Analysis": "Морфологиялык талдоо",
44 | "Morphological_Analysis_Help": "Синтездөгүңүз келген сөзформалар киргизиңиз.",
45 | "Analyze": "Талдоо",
46 | "Morphological_Generation": "Морфологиялык синтездөө",
47 | "Generate": "Синтездөө",
48 | "Spell_Checker": "Катаны текшерүү",
49 | "APy_Sandbox": "APy кум аянтчасы",
50 | "APy_Sandbox_Help": "Каалаган сурамжылоону жөнөтүү",
51 | "APy_Request": "APy сурамжылоосу",
52 | "Request": "Сурамжылоо",
53 | "Language": "Тил",
54 | "Input_Text": "Киргизилген текст",
55 | "Notice_Mistake": "Кандайдыр бир ката таптыңызбы?",
56 | "Help_Improve": "Апертиумду жакшыртууга жардам бериңиз!",
57 | "Contact_Us": "Эгер катаны табсаңыз, проектти демитүү тууралуу калооңуз болсо же жардамдашкыңыз келсе, бизге кабарлаңыз.",
58 | "Maintainer": "Бул вебсайт {{maintainer}} аркылуу башкарылат.",
59 | "About_Title": "Бул вебсайт жөнүндө",
60 | "Enable_JS_Warning": "Бул сайт JavaScript аркылуу гана иштейт. Эгер Javascriptти иштете албасаңыз, Prompsitтеги котормочуларды колдонсоңуз болот.",
61 | "Not_Found_Error": "Ката 404: Кечириңиз, изделген баракча табылган жок!",
62 | "About": "Проект жөнүндө",
63 | "Download": "Жүктөп алуу",
64 | "Contact": "Байланышуу",
65 | "Documentation": "Документация",
66 | "About_Apertium": "Апертиум жөнүндө",
67 | "Documentation_Para": "Документация викибиздеги Документция барагында табылат. Биз айрым презентация жана илимий баяндама басып чыгарганбыз. Алардын тизмеси викинин Чыгармалар барагында табылат.",
68 | "Apertium_Downloads": "Апертиумду жүктөп алуу",
69 | "Install_Apertium": "Апертиумду орнотуу"
70 | }
71 |
--------------------------------------------------------------------------------
/src/__tests__/App.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { fireEvent, getByRole, render, screen, waitFor } from '@testing-library/react';
3 | import mockAxios from 'jest-mock-axios';
4 | import userEvent from '@testing-library/user-event';
5 |
6 | import App from '../App';
7 |
8 | const renderApp = () => {
9 | window.history.replaceState(null, '', '/index.eng.html');
10 |
11 | render(
12 | <>
13 |
14 |
15 | >,
16 | );
17 | };
18 |
19 | const selectLocale = (name: string) => {
20 | const selector = screen
21 | .getAllByRole('combobox')
22 | .find((e) => Array.from(e.childNodes).some((c) => (c as HTMLOptionElement).value === 'eng')) as HTMLSelectElement;
23 | userEvent.selectOptions(selector, getByRole(selector, 'option', { name }));
24 | };
25 |
26 | describe('document drag', () => {
27 | it('switches to doc translation on first hover', () => {
28 | renderApp();
29 |
30 | const body = document.getElementsByTagName('body')[0];
31 | fireEvent(body, new Event('dragenter'));
32 |
33 | expect(window.location.hash).toMatch(/^#docTranslation/);
34 | });
35 |
36 | it('ignores second hover', () => {
37 | renderApp();
38 |
39 | const initialLength = window.history.length;
40 |
41 | const body = document.getElementsByTagName('body')[0];
42 | fireEvent(body, new Event('dragenter'));
43 |
44 | const postInitialDragLength = window.history.length;
45 | expect(postInitialDragLength).toBeGreaterThan(initialLength);
46 |
47 | fireEvent(body, new Event('dragenter'));
48 | expect(window.history.length).toEqual(postInitialDragLength);
49 | });
50 | });
51 |
52 | describe('document level attributes', () => {
53 | it('sets on render', () => {
54 | renderApp();
55 |
56 | expect(document.title).toMatchInlineSnapshot(`"title-Default"`);
57 |
58 | const html = document.getElementsByTagName('html')[0];
59 | expect(html.dir).toBe('ltr');
60 | expect(html.lang).toBe('en');
61 |
62 | expect((document.getElementById('meta-description') as HTMLMetaElement).content).toMatchInlineSnapshot(
63 | `"description-Default"`,
64 | );
65 | });
66 |
67 | it('falls back to alpha3 code for lang', () => {
68 | renderApp();
69 |
70 | selectLocale('arpetan');
71 |
72 | const html = document.getElementsByTagName('html')[0];
73 | expect(html.lang).toBe('frp');
74 | });
75 | });
76 |
77 | describe('changing locale', () => {
78 | it('switches on select', async () => {
79 | renderApp();
80 |
81 | selectLocale('español');
82 |
83 | const title = 'title-Spanish';
84 | mockAxios.mockResponse({ data: { title } });
85 | await waitFor(() => expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({ url: 'strings/spa.json' })));
86 |
87 | await waitFor(() => expect(document.title).toBe(title));
88 | });
89 |
90 | it('falls back to defaults on error', async () => {
91 | renderApp();
92 |
93 | const defaultTitle = document.title;
94 |
95 | selectLocale('español');
96 |
97 | const title = 'title-Spanish';
98 | mockAxios.mockResponse({ data: { title } });
99 | await waitFor(() => expect(mockAxios).toHaveBeenCalled());
100 | await waitFor(() => expect(document.title).toBe(title));
101 |
102 | selectLocale('arpetan');
103 | mockAxios.mockError();
104 |
105 | await waitFor(() => expect(document.title).toBe(defaultTitle));
106 | });
107 |
108 | it('caches results', async () => {
109 | renderApp();
110 |
111 | selectLocale('español');
112 |
113 | mockAxios.mockResponse({ data: { title: 'title-Spanish' } });
114 | await waitFor(() => expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({ url: 'strings/spa.json' })));
115 |
116 | selectLocale('English');
117 | selectLocale('español');
118 |
119 | expect(mockAxios.queue()).toHaveLength(0);
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/src/components/translator/WithSortedLanguages.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { NamedLangs, Pairs, isPair } from '.';
3 |
4 | import { isVariant, parentLang, toAlpha2Code, variantSeperator } from '../../util/languages';
5 | import { LocaleContext } from '../../context';
6 | import { useLocalization } from '../../util/localization';
7 |
8 | export type Props = {
9 | pairs: Pairs;
10 | srcLang: string;
11 | srcLangs: Set;
12 | tgtLangs: Set;
13 | children: (props: ChildProps) => React.ReactElement;
14 | };
15 |
16 | export type ChildProps = {
17 | srcLangs: NamedLangs;
18 | tgtLangs: NamedLangs;
19 | };
20 |
21 | const WithSortedLanguages = ({ pairs, srcLang, srcLangs, tgtLangs, children }: Props): React.ReactElement => {
22 | const { tLang } = useLocalization();
23 | const locale = React.useContext(LocaleContext);
24 |
25 | const collator = React.useMemo(() => new Intl.Collator((toAlpha2Code(locale) || locale).replace('_', '-')), [locale]);
26 |
27 | const compareLangCodes = React.useCallback(
28 | (a: string, b: string): number => {
29 | const [aParent, aVariant] = a.split(variantSeperator, 2),
30 | [bParent, bVariant] = b.split(variantSeperator, 2);
31 | if (aVariant && bVariant && aParent === bParent) {
32 | return collator.compare(tLang(a), tLang(b));
33 | } else if (aVariant && aParent === b) {
34 | return 1;
35 | } else if (bVariant && bParent === a) {
36 | return -1;
37 | } else {
38 | return collator.compare(tLang(aParent), tLang(bParent));
39 | }
40 | },
41 | [collator, tLang],
42 | );
43 |
44 | const sortedSrcLangs: NamedLangs = React.useMemo(
45 | () => [...srcLangs].sort(compareLangCodes).map((code) => [code, tLang(code)]),
46 | [compareLangCodes, srcLangs, tLang],
47 | );
48 |
49 | const possibleTgtFamilies: Set = React.useMemo(
50 | () =>
51 | new Set(
52 | Array.from(tgtLangs).filter((lang) => {
53 | const parent = parentLang(lang);
54 | const possibleTgtLangs = Array.from(pairs[srcLang]);
55 |
56 | return (
57 | isPair(pairs, srcLang, lang) ||
58 | possibleTgtLangs.includes(parent) ||
59 | possibleTgtLangs.some((possibleLang) => parentLang(possibleLang) === parent)
60 | );
61 | }),
62 | ),
63 | [pairs, srcLang, tgtLangs],
64 | );
65 |
66 | const sortedTgtLangs: NamedLangs = React.useMemo(
67 | () =>
68 | [...tgtLangs]
69 | .sort((a, b) => {
70 | const aParent = parentLang(a),
71 | bParent = parentLang(b);
72 |
73 | const aFamilyPossible = possibleTgtFamilies.has(aParent),
74 | bFamilyPossible = possibleTgtFamilies.has(bParent);
75 |
76 | if (aFamilyPossible === bFamilyPossible) {
77 | if (aParent === bParent) {
78 | const aVariant = isVariant(a),
79 | bVariant = isVariant(b);
80 | if (aVariant && bVariant) {
81 | const aPossible = isPair(pairs, srcLang, a),
82 | bPossible = isPair(pairs, srcLang, b);
83 | if (aPossible === bPossible) {
84 | return compareLangCodes(a, b);
85 | } else if (aPossible) {
86 | return -1;
87 | } else {
88 | return 1;
89 | }
90 | } else if (aVariant) {
91 | return 1;
92 | } else {
93 | return -1;
94 | }
95 | } else {
96 | return compareLangCodes(a, b);
97 | }
98 | } else if (aFamilyPossible) {
99 | return -1;
100 | } else {
101 | return 1;
102 | }
103 | })
104 | .map((code) => [code, tLang(code)]),
105 | [compareLangCodes, pairs, possibleTgtFamilies, srcLang, tLang, tgtLangs],
106 | );
107 |
108 | return children({ srcLangs: sortedSrcLangs, tgtLangs: sortedTgtLangs });
109 | };
110 |
111 | export default WithSortedLanguages;
112 |
--------------------------------------------------------------------------------
/src/components/WithInstallationAlert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { AxiosPromise, CancelTokenSource } from 'axios';
3 | import Alert from 'react-bootstrap/Alert';
4 |
5 | import { APyContext, ConfigContext } from '../context';
6 | import { apyFetch } from '../util';
7 | import { useLocalization } from '../util/localization';
8 |
9 | const requestBufferLength = 5,
10 | individualDurationThreshold = 4000,
11 | cumulativeDurationThreshold = 3000,
12 | notificationDuration = 10000;
13 |
14 | const InstallationAlert = ({ show, onClose }: { show: boolean; onClose: () => void }): React.ReactElement => {
15 | const { t } = useLocalization();
16 |
17 | const openTimeoutRef = React.useRef(null);
18 |
19 | const scheduleClose = React.useCallback(() => {
20 | if (!openTimeoutRef.current) {
21 | openTimeoutRef.current = window.setTimeout(onClose, notificationDuration);
22 | }
23 | }, [onClose]);
24 |
25 | React.useEffect(() => {
26 | const { current } = openTimeoutRef;
27 | if (show) {
28 | scheduleClose();
29 | } else if (current) {
30 | window.clearTimeout(current);
31 | openTimeoutRef.current = null;
32 | }
33 |
34 | return () => {
35 | if (openTimeoutRef.current) {
36 | window.clearTimeout(openTimeoutRef.current);
37 | }
38 | };
39 | }, [onClose, scheduleClose, show]);
40 |
41 | const onMouseOver = React.useCallback(() => {
42 | const { current } = openTimeoutRef;
43 | if (current) {
44 | window.clearTimeout(current);
45 | openTimeoutRef.current = null;
46 | }
47 | }, []);
48 |
49 | const onMouseOut = React.useCallback(() => {
50 | if (show) {
51 | scheduleClose();
52 | }
53 | }, [scheduleClose, show]);
54 |
55 | return (
56 |
67 | {t('Install_Apertium')}
68 |
69 |
70 | );
71 | };
72 |
73 | const WithInstallationAlert = ({ children }: { children?: React.ReactNode }): React.ReactElement => {
74 | const { apyURL } = React.useContext(ConfigContext);
75 |
76 | const [show, setShow] = React.useState(false);
77 |
78 | const requestTimings = React.useRef>([]);
79 |
80 | const wrappedApyFetch = React.useCallback(
81 | (path: string, params?: Record): [CancelTokenSource, AxiosPromise] => {
82 | const start = Date.now();
83 |
84 | const handleRequestComplete = () => {
85 | const duration = Date.now() - start;
86 | const timings = requestTimings.current;
87 |
88 | let cumulativeAPyDuration = 0;
89 |
90 | if (timings.length === requestBufferLength) {
91 | cumulativeAPyDuration = timings.reduce((totalDuration, duration) => totalDuration + duration);
92 |
93 | timings.shift();
94 | timings.push(duration);
95 | } else {
96 | timings.push(duration);
97 | }
98 |
99 | const averageDuration = cumulativeAPyDuration / timings.length;
100 |
101 | if (duration > individualDurationThreshold || averageDuration > cumulativeDurationThreshold) {
102 | setShow(true);
103 | }
104 | };
105 |
106 | const [cancel, request] = apyFetch(`${apyURL}/${path}`, params);
107 | return [cancel, request.finally(handleRequestComplete)];
108 | },
109 | [apyURL],
110 | );
111 |
112 | return (
113 | <>
114 |
115 | setShow(false)} show={show} />
116 | {children}
117 |
118 | >
119 | );
120 | };
121 |
122 | export default WithInstallationAlert;
123 |
--------------------------------------------------------------------------------
/src/util/__tests__/useLocalStorage.test.ts:
--------------------------------------------------------------------------------
1 | import { RenderResult, act, renderHook } from '@testing-library/react-hooks';
2 |
3 | import useLocalStorage from '../useLocalStorage';
4 |
5 | const localStorageKey = () => Math.random().toString(36);
6 |
7 | declare global {
8 | // eslint-disable-next-line @typescript-eslint/no-namespace
9 | namespace jest {
10 | interface Matchers {
11 | noRenderError(): R;
12 | renderValueToBe(value: T): R;
13 | }
14 | }
15 | }
16 |
17 | expect.extend({
18 | noRenderError(result: RenderResult<[T, React.Dispatch>]>) {
19 | return {
20 | message: () => `expected error to be empty`,
21 | pass: result.error == null,
22 | };
23 | },
24 | renderValueToBe(result: RenderResult<[T, React.Dispatch>]>, value: T) {
25 | return {
26 | message: () => `expected ${new String(result.current[0]).toString()} to be ${new String(value).toString()}`,
27 | pass: result.current[0] === value,
28 | };
29 | },
30 | });
31 |
32 | it('uses initial value', () => {
33 | const { result } = renderHook(() => useLocalStorage(localStorageKey(), 'bar'));
34 | expect(result).noRenderError();
35 | expect(result).renderValueToBe('bar');
36 | });
37 |
38 | it('uses initial function value', () => {
39 | const { result } = renderHook(() => useLocalStorage(localStorageKey(), () => 'bar'));
40 | expect(result).noRenderError();
41 | expect(result).renderValueToBe('bar');
42 | });
43 |
44 | it('sets new value', () => {
45 | const { result } = renderHook(() => useLocalStorage(localStorageKey(), 'bar'));
46 | expect(result).noRenderError();
47 | act(() => result.current[1]('qux'));
48 | expect(result).renderValueToBe('qux');
49 | });
50 |
51 | it('sets new function value', () => {
52 | const { result } = renderHook(() => useLocalStorage(localStorageKey(), 'bar'));
53 | expect(result).noRenderError();
54 | act(() => result.current[1]((s) => s + s));
55 | expect(result).renderValueToBe('barbar');
56 | });
57 |
58 | it('restores saved value', () => {
59 | const key = localStorageKey();
60 | const { result } = renderHook(() => useLocalStorage(key, 'bar'));
61 | expect(result).noRenderError();
62 |
63 | {
64 | const { result } = renderHook(() => useLocalStorage(key, 'qux'));
65 | expect(result).noRenderError();
66 | expect(result).renderValueToBe('bar');
67 | }
68 | });
69 |
70 | it('uses override value over initial value', () => {
71 | const { result } = renderHook(() => useLocalStorage(localStorageKey(), 'bar', { overrideValue: 'qux' }));
72 | expect(result).noRenderError();
73 | expect(result).renderValueToBe('qux');
74 | });
75 |
76 | it('uses override value over saved value', () => {
77 | const key = localStorageKey();
78 | const { result } = renderHook(() => useLocalStorage(key, 'bar'));
79 | expect(result).noRenderError();
80 |
81 | {
82 | const { result } = renderHook(() => useLocalStorage(key, 'bar', { overrideValue: 'qux' }));
83 | expect(result).noRenderError();
84 | expect(result).renderValueToBe('qux');
85 | }
86 | });
87 |
88 | it('discards saved value that fails validation', () => {
89 | const key = localStorageKey();
90 | const { result } = renderHook(() => useLocalStorage(key, 'bar'));
91 | expect(result).noRenderError();
92 |
93 | {
94 | const { result } = renderHook(() => useLocalStorage(key, 'barr', { validateValue: (s) => s.length === 4 }));
95 | expect(result).noRenderError();
96 | expect(result).renderValueToBe('barr');
97 | }
98 | });
99 |
100 | it('discards override value that fails validation', () => {
101 | const { result } = renderHook(() =>
102 | useLocalStorage(localStorageKey(), 'bar', { overrideValue: 'barr', validateValue: (s) => s.length === 3 }),
103 | );
104 | expect(result).noRenderError();
105 | expect(result).renderValueToBe('bar');
106 | });
107 |
108 | it('discards invalid saved value', () => {
109 | const key = localStorageKey();
110 | window.localStorage.setItem(key, 'I AM NOT VALID JSON');
111 | const { result } = renderHook(() => useLocalStorage(key, 'bar'));
112 | expect(result).noRenderError();
113 | expect(result).renderValueToBe('bar');
114 | });
115 |
--------------------------------------------------------------------------------
/src/components/translator/TranslationOptions.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Col from 'react-bootstrap/Col';
3 | import DropdownButton from 'react-bootstrap/DropdownButton';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 | import Form from 'react-bootstrap/Form';
6 | import Row from 'react-bootstrap/Row';
7 | import { faCog } from '@fortawesome/free-solid-svg-icons';
8 |
9 | import { ConfigContext, LocaleContext } from '../../context';
10 | import { PairPrefValues, getPairPrefs } from '.';
11 | import { useLocalization } from '../../util/localization';
12 |
13 | export type Props = {
14 | markUnknown: boolean;
15 | setMarkUnknown: React.Dispatch>;
16 | instantTranslation: boolean;
17 | setInstantTranslation: React.Dispatch>;
18 | translationChaining: boolean;
19 | setTranslationChaining: React.Dispatch>;
20 | srcLang: string;
21 | tgtLang: string;
22 | pairPrefs: PairPrefValues;
23 | setPairPrefs: (prefs: PairPrefValues) => void;
24 | };
25 |
26 | const TranslationOptions = ({
27 | markUnknown,
28 | setMarkUnknown,
29 | instantTranslation,
30 | setInstantTranslation,
31 | translationChaining,
32 | setTranslationChaining,
33 | srcLang,
34 | tgtLang,
35 | pairPrefs,
36 | setPairPrefs,
37 | }: Props): React.ReactElement => {
38 | const { t } = useLocalization();
39 | const locale = React.useContext(LocaleContext);
40 | const config = React.useContext(ConfigContext);
41 |
42 | const prefs = React.useMemo(() => getPairPrefs(locale, srcLang, tgtLang), [locale, srcLang, tgtLang]);
43 | const [showPrefDropdown, setShowPrefDropdown] = React.useState(false);
44 |
45 | return (
46 | <>
47 | {Object.keys(prefs).length > 0 && (
48 | {
52 | if (isOpen) {
53 | setShowPrefDropdown(true);
54 | }
55 |
56 | if (source === 'rootClose') {
57 | setShowPrefDropdown(false);
58 | }
59 | }}
60 | show={showPrefDropdown}
61 | size="sm"
62 | title={
63 | <>
64 | {t('Norm_Preferences')}
65 | >
66 | }
67 | variant="secondary"
68 | >
69 |
77 | {Object.entries(prefs).map(([id, description]) => (
78 |
79 | setPairPrefs({ ...pairPrefs, [id]: currentTarget.checked })}
87 | style={{ wordBreak: 'break-word' }}
88 | />
89 |
90 | ))}
91 |
92 |
93 | )}
94 | setMarkUnknown(currentTarget.checked)}
100 | />
101 | setInstantTranslation(currentTarget.checked)}
107 | />
108 | {config.translationChaining && (
109 | }
114 | onChange={({ currentTarget }) => setTranslationChaining(currentTarget.checked)}
115 | />
116 | )}
117 | >
118 | );
119 | };
120 |
121 | export default TranslationOptions;
122 |
--------------------------------------------------------------------------------
/src/components/translator/__tests__/TranslationOptions.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import TranslationOptions, { Props } from '../TranslationOptions';
6 |
7 | const renderTranslationOptions = (props_: Partial = {}) => {
8 | const props = {
9 | markUnknown: false,
10 | setMarkUnknown: jest.fn(),
11 | instantTranslation: false,
12 | setInstantTranslation: jest.fn(),
13 | translationChaining: false,
14 | setTranslationChaining: jest.fn(),
15 | srcLang: 'eng',
16 | tgtLang: 'spa',
17 | pairPrefs: {},
18 | setPairPrefs: jest.fn(),
19 | ...props_,
20 | };
21 |
22 | render( );
23 |
24 | return props;
25 | };
26 |
27 | describe('mark unknown', () => {
28 | const name = 'Mark_Unknown_Words-Default';
29 |
30 | it.each([false, true])('renders %s', (markUnknown) => {
31 | renderTranslationOptions({ markUnknown });
32 | expect(screen.getByRole('checkbox', { name, checked: markUnknown })).toBeDefined();
33 | });
34 |
35 | it.each([false, true])('toggles from %s', (markUnknown) => {
36 | const props = renderTranslationOptions({ markUnknown });
37 |
38 | userEvent.click(screen.getByRole('checkbox', { name }));
39 |
40 | expect(props.setMarkUnknown).toHaveBeenCalledWith(!markUnknown);
41 | });
42 | });
43 |
44 | describe('instant translation', () => {
45 | const name = 'Instant_Translation-Default';
46 |
47 | it.each([false, true])('renders %s', (instantTranslation) => {
48 | renderTranslationOptions({ instantTranslation });
49 | expect(screen.getByRole('checkbox', { name, checked: instantTranslation })).toBeDefined();
50 | });
51 |
52 | it.each([false, true])('toggles from %s', (instantTranslation) => {
53 | const { setInstantTranslation } = renderTranslationOptions({ instantTranslation });
54 |
55 | userEvent.click(screen.getByRole('checkbox', { name }));
56 |
57 | expect(setInstantTranslation).toHaveBeenCalledWith(!instantTranslation);
58 | });
59 | });
60 |
61 | describe('translation chaining', () => {
62 | const name = 'Multi_Step_Translation-Default';
63 |
64 | it.each([false, true])('renders %s', (translationChaining) => {
65 | renderTranslationOptions({ translationChaining });
66 | expect(screen.getByRole('checkbox', { name, checked: translationChaining })).toBeDefined();
67 | });
68 |
69 | it.each([false, true])('toggles from %s', (translationChaining) => {
70 | const { setTranslationChaining } = renderTranslationOptions({ translationChaining });
71 |
72 | userEvent.click(screen.getByRole('checkbox', { name }));
73 |
74 | expect(setTranslationChaining).toHaveBeenCalledWith(!translationChaining);
75 | });
76 | });
77 |
78 | describe('pair prefs', () => {
79 | const withPrefsOptions = { srcLang: 'eng', tgtLang: 'cat' };
80 |
81 | it('renders nothing when prefs are unavailable', () => {
82 | renderTranslationOptions();
83 | expect(screen.queryByRole('button')).toBeNull();
84 | });
85 |
86 | it('renders closed dropdown when prefs are available', () => {
87 | renderTranslationOptions(withPrefsOptions);
88 | expect(screen.getByRole('button').textContent).toBe(' Norm_Preferences-Default');
89 | });
90 |
91 | it('renders prefs when dropdown clicked', async () => {
92 | renderTranslationOptions(withPrefsOptions);
93 | userEvent.click(screen.getByRole('button'));
94 |
95 | expect(await screen.findByRole('checkbox', { name: 'foo_pref' })).toBeDefined();
96 | });
97 |
98 | it('closes dropdown when clicked', () => {
99 | renderTranslationOptions(withPrefsOptions);
100 |
101 | const button = screen.getByRole('button');
102 | userEvent.click(button);
103 | expect(button.getAttribute('aria-expanded')).toBe('true');
104 |
105 | userEvent.click(button);
106 | expect(button.getAttribute('aria-expanded')).toBe('false');
107 | });
108 |
109 | it('updates prefs when checkbox clicked', async () => {
110 | const { setPairPrefs } = renderTranslationOptions(withPrefsOptions);
111 |
112 | userEvent.click(screen.getByRole('button'));
113 | userEvent.click(await screen.findByRole('checkbox', { name: 'foo_pref' }));
114 |
115 | expect(setPairPrefs).toBeCalledWith({ foo: true });
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/src/components/footer/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { getAllByRole, render, screen, waitFor } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import Config from '../../../../config';
6 | import { ConfigContext } from '../../../context';
7 | import { Config as ConfigType } from '../../../types';
8 |
9 | import Footer from '..';
10 |
11 | const renderFooter = (config: Partial = {}) => {
12 | const wrapRef = React.createRef();
13 | const pushRef = React.createRef();
14 |
15 | render(
16 |
17 | <>
18 |
21 |
22 | >
23 | ,
24 | );
25 | };
26 |
27 | describe('Footer', () => {
28 | it('renders with navigation buttons', () => {
29 | renderFooter();
30 | const navigation = screen.getByRole('navigation');
31 | const buttons = getAllByRole(navigation, 'button');
32 | expect(buttons).toHaveLength(4);
33 | });
34 |
35 | it('closes dialogs on click', async () => {
36 | renderFooter();
37 |
38 | userEvent.click(screen.getByRole('button', { name: 'About-Default' }));
39 | expect(screen.getByRole('dialog')).toBeDefined();
40 |
41 | userEvent.click(screen.getByRole('button', { name: 'Close' }));
42 | await waitFor(() => expect(screen.queryByRole('dialog')?.style.opacity).toBe(''));
43 | });
44 |
45 | describe('navigation buttons', () => {
46 | it('opens about dialog and display show more languages link when showMoreLanguagesLink is set to true', () => {
47 | renderFooter({ showMoreLanguagesLink: true });
48 |
49 | userEvent.click(screen.getByRole('button', { name: 'About-Default' }));
50 |
51 | expect(screen.getByRole('dialog').textContent).toMatchInlineSnapshot(
52 | `"About_Apertium-Default×CloseWhat_Is_Apertium-DefaultApertium-DefaultMore_Languages-Default"`,
53 | );
54 | });
55 |
56 | it('opens about dialog and does not display show more languages link when showMoreLanguagesLink is set to false', () => {
57 | renderFooter({ showMoreLanguagesLink: false });
58 |
59 | userEvent.click(screen.getByRole('button', { name: 'About-Default' }));
60 |
61 | expect(screen.getByRole('dialog').textContent).toMatchInlineSnapshot(
62 | `"About_Apertium-Default×CloseWhat_Is_Apertium-DefaultApertium-Default"`,
63 | );
64 | });
65 |
66 | it('opens download dialog', () => {
67 | renderFooter();
68 |
69 | userEvent.click(screen.getByRole('button', { name: 'Download-Default' }));
70 |
71 | expect(screen.getByRole('dialog').textContent).toMatchInlineSnapshot(
72 | `"Apertium_Downloads-Default×CloseDownloads_Para-Default"`,
73 | );
74 | });
75 |
76 | it('opens documentation dialog', () => {
77 | renderFooter();
78 |
79 | userEvent.click(screen.getByRole('button', { name: 'Documentation-Default' }));
80 |
81 | expect(screen.getByRole('dialog').textContent).toMatchInlineSnapshot(
82 | `"Apertium_Documentation-Default×CloseDocumentation_Para-Default"`,
83 | );
84 | });
85 |
86 | it('opens contact dialog', () => {
87 | renderFooter();
88 |
89 | userEvent.click(screen.getByRole('button', { name: 'Contact-Default' }));
90 |
91 | expect(screen.getByRole('dialog').textContent).toMatchInlineSnapshot(
92 | `"Contact-Default×CloseContact_Us-DefaultContact_Para-Default"`,
93 | );
94 | });
95 | });
96 |
97 | describe('help improve buttons', () => {
98 | it('opens about dialog on mobile', () => {
99 | renderFooter();
100 |
101 | userEvent.click(screen.getAllByRole('button', { name: 'Help_Improve-Default' })[0]);
102 |
103 | expect(screen.getByRole('dialog').textContent).toMatchInlineSnapshot(
104 | `"About_Apertium-Default×CloseWhat_Is_Apertium-DefaultApertium-Default"`,
105 | );
106 | });
107 |
108 | it('opens about dialog on desktop', () => {
109 | renderFooter();
110 |
111 | userEvent.click(screen.getAllByRole('button', { name: 'Help_Improve-Default' })[0]);
112 |
113 | expect(screen.getByRole('dialog').textContent).toMatchInlineSnapshot(
114 | `"About_Apertium-Default×CloseWhat_Is_Apertium-DefaultApertium-Default"`,
115 | );
116 | });
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/src/components/footer/AboutModal.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Modal, { ModalProps } from 'react-bootstrap/Modal';
3 | import Col from 'react-bootstrap/Col';
4 | import Row from 'react-bootstrap/Row';
5 |
6 | import { ConfigContext } from '../../context';
7 | import { useLocalization } from '../../util/localization';
8 |
9 | import alicanteLogo from './img/logouapp.gif';
10 | import bytemarkLogo from './img/logo_bytemark.gif';
11 | import catalunyaLogo from './img/stsi.gif';
12 | import ccLogo from './img/cc-by-sa-3.0-88x31.png';
13 | import gciLogo from './img/GCI_logo.png';
14 | import githubLogo from './img/github.png';
15 | import gplLogo from './img/gplv3-88x31.png';
16 | import gsocLogo from './img/GSOC_logo.svg';
17 | import maeLogo from './img/logo_mae_ro_75pc.jpg';
18 | import mineturLogo from './img/logomitc120.jpg';
19 | import prompsitLogo from './img/prompsit150x52.png';
20 |
21 | const AboutModal = (props: ModalProps): React.ReactElement => {
22 | const config = React.useContext(ConfigContext);
23 | const { t } = useLocalization();
24 |
25 | return (
26 |
27 |
28 | {t('About_Apertium')}
29 |
30 |
31 |
32 |
33 | {config.showMoreLanguagesLink &&
}
34 |
35 |
36 |
37 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
96 |
97 |
102 |
103 |
104 |
105 | );
106 | };
107 |
108 | export default AboutModal;
109 |
--------------------------------------------------------------------------------
/src/components/Sandbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import axios, { CancelTokenSource } from 'axios';
3 | import Button from 'react-bootstrap/Button';
4 | import Col from 'react-bootstrap/Col';
5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6 | import Form from 'react-bootstrap/Form';
7 | import classNames from 'classnames';
8 | import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
9 |
10 | import { APyContext } from '../context';
11 | import ErrorAlert from './ErrorAlert';
12 | import useLocalStorage from '../util/useLocalStorage';
13 | import { useLocalization } from '../util/localization';
14 |
15 | const SandboxForm = ({
16 | setLoading,
17 | setResult,
18 | setError,
19 | }: {
20 | setLoading: React.Dispatch>;
21 | setResult: React.Dispatch>;
22 | setError: React.Dispatch>;
23 | }): React.ReactElement => {
24 | const { t } = useLocalization();
25 | const apyFetch = React.useContext(APyContext);
26 |
27 | const [requestText, setRequestText] = useLocalStorage('sandboxRequest', '');
28 |
29 | const requestRef = React.useRef(null);
30 |
31 | const handleSubmit = () => {
32 | if (requestText.trim().length === 0) {
33 | return;
34 | }
35 |
36 | requestRef.current?.cancel();
37 | requestRef.current = null;
38 |
39 | void (async () => {
40 | try {
41 | setLoading(true);
42 | const [ref, request] = apyFetch(requestText);
43 | requestRef.current = ref;
44 |
45 | const startTime = Date.now();
46 | const response = await request;
47 | const requestTime = Date.now() - startTime;
48 |
49 | setResult([JSON.stringify(response.data, undefined, 3), requestTime]);
50 | setError(null);
51 | setLoading(false);
52 |
53 | requestRef.current = null;
54 | } catch (error) {
55 | if (!axios.isCancel(error)) {
56 | setResult(null);
57 | setError(error as Error);
58 | setLoading(false);
59 | }
60 | }
61 | })();
62 | };
63 |
64 | return (
65 |
73 | {t('APy_Request')}
74 |
75 | setRequestText(value)}
79 | onKeyDown={(event: React.KeyboardEvent) => {
80 | if (event.code === 'Enter' && !event.shiftKey) {
81 | event.preventDefault();
82 | handleSubmit();
83 | }
84 | }}
85 | required
86 | rows={3}
87 | spellCheck={false}
88 | value={requestText}
89 | />
90 |
91 | {'e.g. /perWord?lang=en-es&modes=morph+translate+biltrans&q=let+there+be+light'}
92 |
93 |
94 |
95 |
96 |
97 |
98 | {t('Request')}
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | const Sandbox = (): React.ReactElement => {
107 | const [loading, setLoading] = React.useState(false);
108 | const [result, setResult] = React.useState<[string, number] | null>(null);
109 | const [error, setError] = React.useState(null);
110 |
111 | return (
112 | <>
113 |
114 |
115 |
116 | {result && (
117 | <>
118 |
119 | {result[1]}
120 | {'ms'}
121 |
122 | {result[0]}
123 | >
124 | )}
125 | {error && }
126 |
127 | >
128 | );
129 | };
130 |
131 | export default Sandbox;
132 |
--------------------------------------------------------------------------------
/src/strings/heb.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "n0nick"
5 | ],
6 | "last-updated": "2014-05-13",
7 | "locale": [
8 | "he",
9 | "heb"
10 | ],
11 | "completion": " 66% 67.69%",
12 | "missing": [
13 | "Apertium_Documentation",
14 | "Cancel",
15 | "Drop_Document",
16 | "File_Too_Large",
17 | "Format_Not_Supported",
18 | "Install_Apertium",
19 | "Install_Apertium_Para",
20 | "Mark_Unknown_Words",
21 | "More_Languages",
22 | "Morphological_Analysis_Help",
23 | "Morphological_Generation_Help",
24 | "Multi_Step_Translation",
25 | "Norm_Preferences",
26 | "Not_Found_Error",
27 | "Supported_Formats",
28 | "Translate_Document",
29 | "Translate_Webpage",
30 | "Translation_Help",
31 | "description"
32 | ]
33 | },
34 | "title": "Apertium | פלטפורמה חופשית לתרגום מכונה",
35 | "tagline": "פלטפורמה חופשית לתרגום מכונה",
36 | "Translation": "תרגום",
37 | "Translate": "תרגם",
38 | "Detect_Language": "זהה שפה",
39 | "detected": "זוהתה",
40 | "Instant_Translation": "מיידי",
41 | "Not_Available": "התרגום עוד לא זמין!",
42 | "Morphological_Analysis": "ניתוח מורפולוגי",
43 | "Analyze": "נתח",
44 | "Morphological_Generation": "חילול מורפולוגי",
45 | "Generate": "חולל",
46 | "Spell_Checker": "בודק איות",
47 | "APy_Sandbox": "ארגז כלים ל-APy",
48 | "APy_Sandbox_Help": "שליחת בקשות שרירותיות",
49 | "APy_Request": "בקשת APy",
50 | "Request": "בקשה",
51 | "Language": "שפה",
52 | "Input_Text": "טקסט קלט",
53 | "Notice_Mistake": "מצאת טעות?",
54 | "Help_Improve": "עזרו לנו לשפר את Apertium!",
55 | "Contact_Us": "הרגישו חופשי ליצור קשר אם מצאתם טעות, יש פרויקט שתרצו לראות אותנו עובדים עליו, או שאתם מעוניינים להציע עזרה.",
56 | "Maintainer": "אתר זה מתוחזק ע\"י {{maintainer}}.",
57 | "About_Title": "אודות אתר זה",
58 | "Enable_JS_Warning": "אתר זה פועל רק כש-Javascript מופעלת, אם אין באפשרותכם להפעיל את Javacript , תוכלו לנסות את כלי התרגום ב-Prompsit .",
59 | "About": "אודות",
60 | "Download": "הורד",
61 | "Contact": "צור קשר",
62 | "Documentation": "תיעוד",
63 | "About_Apertium": "אודות Apertium",
64 | "What_Is_Apertium": "Apertium היא פלטפורמה חופשית לתרגום מכונה , שנועדה במקור לתרגום בין זוגות של שפות קשורות אך הורחבה לטפל גם בזוגות של שפות רחוקות יותר (כמו אנגלית-קטלאנית). הפלטפורמה מספקת
מנוע תרגום מכונה ללא תלות בשפה מסוימת כלים לניהול המידע הבלשני הדרוש לבניית מערכת תרגום מכונה עבור זוג מסוים של שפות, ו מידע בלשני עבור מספר הולך וגדל של זוגות של שפות. Apertium מקבלת בברכה מפתחים חדשים: אם לדעתכם תוכלו לשפר את המנוע או את הכלים בפרויקט, או לפתח מידע בלשני עבורנו, אל תהססו לפנות אלינו .
",
65 | "Documentation_Para": "תיעוד ניתן למצוא ב-Wiki שלנו תחת הדף Documentation . כמו כן, פרסמנו מאמרים שונים במסגרת כנסים אקדמיים וכתבי עת, תוכלו למצוא רשימה של אלו ב-Wiki תחת Publications .",
66 | "Apertium_Downloads": "הורדות Apertium",
67 | "Downloads_Para": " הגרסאות האחרונות של ארגז הכלים של Apertium , כמו גם מידע עבור זוגות שפות זמינים לעיון והורדה בדף הפרויקט ב-GitHub . הוראות ההתקנה של Apertium עבור עיקר הפלטפורמות מפורטות בדף ההתקנה ב-Wiki .",
68 | "Contact_Para": "ערוץ IRC הדרך הזריזה ביותר לפנות אלינו היא בהצטרפות לערוץ ה-IRC שלנו, #apertium ב-irc.oftc.net, בו נפגשים המשתמשים והמפתחים של Apertium. אין צורך בתוכנה מיוחדת; ניתן להשתמש ב-OFTC webchat .
רשימת דיוור כמו כן, תוכלו להירשם לרשימת הדיוור apertium-stuff , שם תוכלו לכתוב בפירוט לגבי הצעות או בעיות, כמו גם לעקוב אחר דיונים כלליים בנושא Apertium.
יצירת קשר הרגישו חופשי ליצור קשר באמצעות רשימת הדיוור apertium-contact אם מצאתם טעות, יש פרויקט שתרצו לראות אותנו עובדים עליו, או שאתם מעוניינים להציע עזרה.
"
69 | }
70 |
--------------------------------------------------------------------------------
/src/strings/tha.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [],
4 | "last-updated": "",
5 | "locale": [
6 | "tha"
7 | ],
8 | "completion": " 75% 74.23%",
9 | "missing": [
10 | "Apertium_Documentation",
11 | "Drop_Document",
12 | "Install_Apertium",
13 | "Install_Apertium_Para",
14 | "Mark_Unknown_Words",
15 | "More_Languages",
16 | "Morphological_Analysis_Help",
17 | "Morphological_Generation_Help",
18 | "Multi_Step_Translation",
19 | "Norm_Preferences",
20 | "Supported_Formats",
21 | "Translate_Webpage",
22 | "Translation_Help",
23 | "description"
24 | ]
25 | },
26 | "title": "อาเปอร์เทียม | แพรตฟอร์มเครื่องแปลภาษาเสรี/โอเพนซอร์ส",
27 | "tagline": "แพรตฟอร์มเครื่องแปลภาษาเสรี/โอเพนซอร์ส",
28 | "Translation": "การแปล",
29 | "Translate": "แปล",
30 | "Detect_Language": "ตรวจหาภาษา",
31 | "detected": "ได้ตรวจหาแล้ว",
32 | "Instant_Translation": "การแปลแบบทันที",
33 | "Translate_Document": "แปลเอกสาร",
34 | "Not_Available": "การแปลยังไม่พร้อม",
35 | "File_Too_Large": "ไฟล์ใหญ่เกินไป",
36 | "Cancel": "ยกเลิก",
37 | "Format_Not_Supported": "รูปแบบยังไม่ถูกรองรับ",
38 | "Morphological_Analysis": "การวิเคราะห์วจีวิภาค",
39 | "Analyze": "วิเคราะห์",
40 | "Morphological_Generation": "การสร้างทางวิจีวิภาค",
41 | "Generate": "สร้าง",
42 | "Spell_Checker": "ตรวจสอบการสะกด",
43 | "APy_Sandbox": "กล่องทรายเอพีวาย",
44 | "APy_Sandbox_Help": "ส่งคำขอกำหนดเอง",
45 | "APy_Request": "คำขอของเอพีวาย",
46 | "Request": "คำขอ",
47 | "Language": "ภาษา",
48 | "Input_Text": "ข้อความนำเข้า",
49 | "Notice_Mistake": "พบข้อผิดพลาด?",
50 | "Help_Improve": "ช่วยเราปรับปรุงอาเปอร์เทียม!",
51 | "Contact_Us": "อย่าลัวเลที่จะติดต่ำเราถ้าคุณพบข้อผิดพลาดหรือมีโครงการที่คุณต้องการเห็นเราทำหรือที่คุณต้องการช่วย",
52 | "Maintainer": "เว็บไซต์นี้บำรุงรักษาโดย {{maintainer}}.",
53 | "About_Title": "เกี่ยวกับเว็บไซต์นี้",
54 | "Enable_JS_Warning": "เว็บนี้ใช้งานได้เฉพาะเมื่อจาวาสคิปต์ถูกเปิดใช้งาน ถ้าคุณไม่สามารถenable Javascript ลองตัวแปลภาษาที่ Prompsit .",
55 | "Not_Found_Error": "ข้อผิดพลาด 404 ขออภัยหน้าเว็บไม่มีอยู่อีกต่อไป",
56 | "About": "เกี่ยวกัย",
57 | "Download": "ดาวโหลด",
58 | "Contact": "ติดต่อ",
59 | "Documentation": "เอกสาร",
60 | "About_Apertium": "เกี่ยวกับอาเปอร์เทียม",
61 | "What_Is_Apertium": "อาเปอร์เทียมคือแพลตฟอร์มเครื่องแปลเสรี/โอเพนซอร์ส แต่เดิมทีมุ่งไปที่คู่ภาษาที่มีความเกี่ยวข้องกันแต่ขยายให้จัดกับกับคู่ภาษาที่ต่างกันมากขึ้นได้ (เช่น ภาษาอังกฤษ-ภาษาคาตาลัน) แพลตฟอร์มให้บริการ
เอนจินเครื่องแปลภาษาที่ไม่ขึ้นกับภาษา เครื่องมือที่ใช้จัดการข้อมูลทางภาษาศาสตร์ที่จำเป็นสำหรับการสร้างระบบเครื่องแปลภาษาสำหรับภาษาที่จะแปลและ ข้อมูลทางภาษาศาสตร์สำหรับเพิ่มจำนวนคู่ภาษา อาเปอร์เทียมยินดีต้อนรับนักพัฒนาใหม่ทั้งหลาย: ถ้าคุณคิดว่าคุณสามารถปรับปรุงเอนจินและเครื่องมือทั้งหลาย หรือพัฒนาข้อมูลทางภาษาศาสตร์สำหรับเรา อย่าลังเลที่จะติดต่อเรา .
",
62 | "Documentation_Para": "เอกสารสามารถหาพบได้ที่วิกิ ในหน้าย่อยเอกสาร เราได้ตีพิมพ์เอกสารในการประชุมวิชาการและบทความในวารสารมากมาย รายการอาจจะพบได้ในวิกิการตีพิมพ์ .",
63 | "Apertium_Downloads": "ดาวโหลดอาเปอร์เทียม",
64 | "Downloads_Para": "รุ่นปัจจุบันของกล่องเครื่องมืออาเปอร์เทีย, where you can post longer proposals or issues,ม อีกทั้งข้อมูลคู่ภาษา อยู่ที่หน้าของ GitHub วิธีการติดตั้งสำหรับอาเปอร์เทียมบนแพลตฟอร์มหลักทั้งหมดมีให้ที่หน้าการติดตั้งที่วิกิ .",
65 | "Contact_Para": "ช่องไออาร์ซี วิธีที่เร็วที่สุดที่จะติดต่อเราโดยเข้าร่วมไออาร์ซี ที่ช่อง #apertium ที่ irc.oftc.net ที่ผู้ใช้และนักพัฒนาทั้งหลายของอาเปอร์เทียมพบกัน คุณไม่จำเป็นต้องใช้ไออาร์ซีไคลเอนต์ คุณสามารถใช้โปรแกรมแชทบนเว็บของฟรีโหนด .
Mailing list อีกทั้งยังสามารถบอกรับเมล์ที่ apertium-stuff mailing list ที่คุณสามารถโพสข้อเสนอหรือประเด็นที่ยาวขึ้น พร้อมทั้งติดตามการอภิปรายโดยทั่วไปของอาเปอร์เทียม
ติดต่อ อย่าลังเลที่จะติดต่อเราโดย apertium-contact mailing list ถ้าคุณพบข้อผิดพลาดหรือมีโครงการที่คุณต้องการเห็นเราทำหรือที่คุณต้องการช่วย
"
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/footer/index.tsx:
--------------------------------------------------------------------------------
1 | import './footer.css';
2 |
3 | import * as React from 'react';
4 | import { faBook, faDownload, faEnvelope, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
5 | import Button from 'react-bootstrap/Button';
6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7 | import { ModalProps } from 'react-bootstrap/Modal';
8 | import Nav from 'react-bootstrap/Nav';
9 |
10 | import AboutModal from './AboutModal';
11 | import ContactModal from './ContactModal';
12 | import DocumentationModal from './DocumentationModal';
13 | import DownloadModal from './DownloadModal';
14 | import { useLocalization } from '../../util/localization';
15 |
16 | // eslint-disable-next-line
17 | const version: string = (window as any).VERSION;
18 |
19 | const enum Tab {
20 | About = 'about',
21 | Download = 'download',
22 | Documentation = 'documentation',
23 | Contact = 'contact',
24 | }
25 |
26 | const Tabs: Record React.ReactElement> = {
27 | [Tab.About]: AboutModal,
28 | [Tab.Download]: DownloadModal,
29 | [Tab.Documentation]: DocumentationModal,
30 | [Tab.Contact]: ContactModal,
31 | };
32 |
33 | const FooterNav_ = ({
34 | setOpenTab,
35 | }: {
36 | setOpenTab: React.Dispatch>;
37 | }): React.ReactElement => {
38 | const { t } = useLocalization();
39 |
40 | return (
41 |
48 |
49 |
50 | setOpenTab(Tab.About)}>
51 | {t('About')}
52 |
53 |
54 |
55 | setOpenTab(Tab.Download)}>
56 | {t('Download')}
57 |
58 |
59 |
60 |
61 |
62 | setOpenTab(Tab.Documentation)}>
63 | {t('Documentation')}
64 |
65 |
66 |
67 | setOpenTab(Tab.Contact)}>
68 | {t('Contact')}
69 |
70 |
71 |
72 |
73 | );
74 | };
75 | const FooterNav = React.memo(FooterNav_);
76 |
77 | const Footer = ({
78 | wrapRef,
79 | pushRef,
80 | }: {
81 | wrapRef: React.RefObject;
82 | pushRef: React.RefObject;
83 | }): React.ReactElement => {
84 | const { t } = useLocalization();
85 |
86 | const [openTab, setOpenTab] = React.useState(undefined);
87 |
88 | const footerRef = React.createRef();
89 | React.useLayoutEffect(() => {
90 | const refreshSizes = () => {
91 | if (pushRef.current && wrapRef.current && footerRef.current) {
92 | const footerHeight = footerRef.current.offsetHeight;
93 | pushRef.current.style.height = `${footerHeight}px`;
94 | wrapRef.current.style.marginBottom = `-${footerHeight}px`;
95 | }
96 | };
97 |
98 | window.addEventListener('resize', refreshSizes);
99 | refreshSizes();
100 |
101 | return () => window.removeEventListener('resize', refreshSizes);
102 | }, [footerRef, wrapRef, pushRef]);
103 |
104 | return (
105 | <>
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | {t('Notice_Mistake')} {' '}
114 | setOpenTab(Tab.About)} tabIndex={0} variant="link">
115 | {t('Help_Improve')}
116 |
117 |
118 |
124 | {version}
125 |
126 |
127 |
128 |
129 |
130 |
131 | {Object.entries(Tabs).map(([tab, Modal]) => (
132 | setOpenTab(undefined)} show={openTab === tab} />
133 | ))}
134 | >
135 | );
136 | };
137 |
138 | export default Footer;
139 |
--------------------------------------------------------------------------------
/src/strings/kaz.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "jonorthwash",
5 | "aida27"
6 | ],
7 | "last-updated": "2014-05-06",
8 | "locale": [
9 | "kk",
10 | "kaz"
11 | ],
12 | "completion": " 66% 76.30%",
13 | "missing": [
14 | "Apertium_Documentation",
15 | "Cancel",
16 | "Drop_Document",
17 | "File_Too_Large",
18 | "Format_Not_Supported",
19 | "Install_Apertium",
20 | "Install_Apertium_Para",
21 | "Mark_Unknown_Words",
22 | "More_Languages",
23 | "Morphological_Analysis_Help",
24 | "Morphological_Generation_Help",
25 | "Multi_Step_Translation",
26 | "Norm_Preferences",
27 | "Not_Found_Error",
28 | "Supported_Formats",
29 | "Translate_Document",
30 | "Translate_Webpage",
31 | "Translation_Help",
32 | "description"
33 | ]
34 | },
35 | "title": "Apertium | A free/open-source machine translation platform",
36 | "tagline": "A free/open-source machine translation platform",
37 | "Translation": "Аударма",
38 | "Translate": "Аудару",
39 | "Detect_Language": "Тілді анықтау",
40 | "detected": "анықталды",
41 | "Instant_Translation": "Шапшаң аударма",
42 | "Not_Available": "Аударма әзірше қолжетімді емес",
43 | "Morphological_Analysis": "Морфологиялық анализ",
44 | "Analyze": "Талдау",
45 | "Morphological_Generation": "Морфологиялық синтез",
46 | "Generate": "Синтездеу",
47 | "Spell_Checker": "Емлені тексергіш",
48 | "APy_Sandbox": "APy құмжәшік",
49 | "APy_Sandbox_Help": "APyге сұрақ қою",
50 | "APy_Request": "APyден сұрау",
51 | "Request": "Сұрау",
52 | "Language": "Тіл",
53 | "Input_Text": "Кіріс мәтін",
54 | "Notice_Mistake": "Қате таптыңыз ба?",
55 | "Help_Improve": "Апертиумды жақсартуға көмектесіңіз!",
56 | "Contact_Us": "Егер қатені тапсаңыз немесе жобаны дамыту туралы тілегіңіз болса, жетілдіру жұмысына көмектескіңіз келсе, бізге хабарлаңыз",
57 | "Maintainer": "Осы веб-сайтты {{maintainer}} сүйемелдейді.",
58 | "About_Title": "Бұл веб-сайт туралы",
59 | "Enable_JS_Warning": "Осы сайт тек қана қосулы JavaScript жұмыс жасайды, егер сізде жоқ болса enable Javascript , одан кейін орындаңыз translators at Prompsit .",
60 | "About": "Жоба туралы",
61 | "Download": "Жүктеп алу",
62 | "Contact": "Байланыс",
63 | "Documentation": "Құжаттама",
64 | "About_Apertium": "Апертиум туралы",
65 | "What_Is_Apertium": "Apertium бұл тегін/ашық-дереккөзді машиналық аударма платформасы , алғашқы кезде байланысқан тілдік жұптар үшін болған, бірақ, кейін көбірек ауытқыған тілдік жұптар(Ағылшын-Каталан сияқты) үшін кеңейтілген болатын. Платформа құралады
тілдік-тәуелсіз машиналық аударма қозғалтқышынан берілген тілдік жұбының машиналық аударма жүйесін құру үшін қажетті лингвистикалық қорды басқаратын құралдардан Апертиум жаңа құрушыларды күтеді: егер сізде қозғалтқышты немесе құралдарды жетілдіру ойыңыз болса немесе бізге лингвистикалық қорды дамыта алсаңыз, онда көп ойламай арқылы бізбен хабарласыңыз .
",
66 | "Documentation_Para": "Құжаттама біздің Wiki сайтының Құжаттама ішкі бетінде таба аласыз. Біз әр түрлі баяндамалар мен журнал мақалаларын шығардық, олардың тізімін Wiki сайтында Басылымдар бөлімінде таба аласыз.",
67 | "Apertium_Downloads": "Апертиумды жүктеп алу",
68 | "Downloads_Para": "Ағымдағы Апертиум құралдарын сондай-ақ тілдік-жұп деректерін GitHub бетінде таба аласыз. Апертиумды негізгі платформаларға орнату үшін нұсқаулықтар Wiki-ның орнату бетінде көрсетілген.",
69 | "Contact_Para": "IRC channel Байланысудың ең тез жолы - біздің IRC арнамызға, #apertium irc.oftc.net сайтында қосылу, бұл жерде Апертиумның қолданушылары мен құраушылары кездеседі. Сізге IRC client қажет емес; қолдану үшін OFTC webchat қосылыңыз.
Жіберу тізімі Сонымен қатар, apertium-stuff mailing list жазылыңыз, бұл жерде сіз ұзынырақ сұраныстар мен мәселелерді жібере аласыз, Апертиумның жалпы талқылауларда сияқты.
Байланысу Бізбен жеңіл хабарласыңыз aпертиум-байланыс жіберу тізімі арқылы, әсіресе, егер сіз біз жасап жатқан жобада қате тапсаңыз немесе осы жобаға көмектескіңіз келсе.
"
70 | }
71 |
--------------------------------------------------------------------------------
/src/strings/uig.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Mardan"
5 | ],
6 | "last-updated": "2014-06-04",
7 | "locale": [
8 | "ug",
9 | "uig"
10 | ],
11 | "completion": " 66% 77.01%",
12 | "missing": [
13 | "Apertium_Documentation",
14 | "Cancel",
15 | "Drop_Document",
16 | "File_Too_Large",
17 | "Format_Not_Supported",
18 | "Install_Apertium",
19 | "Install_Apertium_Para",
20 | "Mark_Unknown_Words",
21 | "More_Languages",
22 | "Morphological_Analysis_Help",
23 | "Morphological_Generation_Help",
24 | "Multi_Step_Translation",
25 | "Norm_Preferences",
26 | "Not_Found_Error",
27 | "Supported_Formats",
28 | "Translate_Document",
29 | "Translate_Webpage",
30 | "Translation_Help",
31 | "description"
32 | ]
33 | },
34 | "title": "Apertium | ھەقسىز ۋە ئوچۇق كودلۇق ماشىنا تەرجىمە سۇپىسى",
35 | "tagline": "ھەقسىز ۋە ئوچۇق كودلۇق ماشىنا تەرجىمە سۇپىسى",
36 | "Translation": "تەرجىمان",
37 | "Translate": "تەرجىمە قىلىش",
38 | "Detect_Language": "ئاپتوماتىك بايقاش",
39 | "detected": "ئاپتوماتىك بايقالغان",
40 | "Instant_Translation": "ھازىر جاۋاب تەرجىمە",
41 | "Not_Available": "ھازىرچە تەرجىمە قىلىنمىدى.",
42 | "Morphological_Analysis": "مورفولوگىيەلىك ئانالىز",
43 | "Analyze": "ئانالىز قىلىش",
44 | "Morphological_Generation": "مورفولوگىيەلىك ھاسىللاش",
45 | "Generate": "ھاسىللاش",
46 | "Spell_Checker": "ئىملاچى",
47 | "APy_Sandbox": "APy sandbox",
48 | "APy_Sandbox_Help": "Send arbitrary requests",
49 | "APy_Request": "APyئىلتىماسى",
50 | "Request": "ئىلتىماس",
51 | "Language": "تىل",
52 | "Input_Text": "مەنبە تېكىست",
53 | "Notice_Mistake": "بىرەر خاتالىق بايقىدىڭىزمۇ؟",
54 | "Help_Improve": "Apertium نى تېخىمۇ مۇكەممەللەشتۈرۈشىمىزگە ياردەملىشىڭ!",
55 | "Contact_Us": "ئەگەر بىرەر خاتالىق بايقىسىڭىز بىز بىلەن ئالاقە قىلغايسىز.",
56 | "Maintainer": "مەزكۇر بېكەت {{maintainer}} تەرىپىدىن ئاسرىلىدۇ.",
57 | "About_Title": "مەزكۇر بېكەت ھەققىدە",
58 | "Enable_JS_Warning": "مەزكۇر بېكەتنىڭ تورمال ئىشلىتىش ئۈچۈن تور كۆرگۈچنىڭ JavaScript ئىقتىدارى ئوچۇق بولۇشى كېرەك. ئەگەر Javascript نى ئېچىشتا قېيىنچىلىققا ئۇچرىسىڭىز Prompsit دىكى تەرجىمانلارنى سىناپ كۆرۈڭ.",
59 | "About": "ھەققىدە",
60 | "Download": "چۈشۈرۈش",
61 | "Contact": "ئالاقىلىشىش",
62 | "Documentation": "قوللانما",
63 | "About_Apertium": "Apertium ھەققىدە",
64 | "What_Is_Apertium": "Apertium بولسا ھەقسىز ۋە ئوچۇق كودلۇق ماشىنا تەرجىمە سۇپىسى بولۇپ، ئەڭ دەسلەپتە يېقىن تىللار ئارىسىدىكى تەرجىمە ئۈچۈن تۈزۈلگەن ۋە كېينچە تېخىمۇ كېڭەيتىلىپ خىلمۇخىل تىللارنى قوللايدىغان بولغان (مەسىلەن ئىنگلىزچە-كاتالانچە).
تىلغا بېقنمايدىغان ماشىنا تەرجىمە ماتورى ماشىنا تەرجىمە سىستېمىسىنى قۇرۇشقا كېرەكلىك بولىدىغان تىل ئۇچۇرلىرىنى باشقۇرۇش قۇراللىرى ئۈزلۈكسىز توپلىنىۋاتقان تىل ئۇچۇرلىرى Apertium ئىجادىيەت سېپىگە قوشۇلۇشىڭىزنى قارشى ئالىمىز. ئەگەر تەرجىمە ماتورى ياكى قۇراللارنىڭ مۇكەممەلىشىشىگە ياردەم قىلالىسىڭىز ۋە ياكى تىل ئۇچۇر ئامبارلىرىنى قۇرالىسىڭىز بىز بىلەن ئالاقە قىلغايسىز .
",
65 | "Documentation_Para": "قوللانمىنى تور بەت ۋىكىسىنىڭ Documentation ناملىق تارماق بېتىدىن تاپالايسىز. بىز يەنە بىر قىسىم يىغىن ۋە ژورناللاردا ماقالە ئېلان قىلدۇق. ئۇلارنىڭ تىزىملىكىنى Publications بېتىدىن تاپالايسىز.",
66 | "Apertium_Downloads": "Apertium نى چۈشۈرۈش",
67 | "Downloads_Para": "Apertium toolbox ۋە language-pair data نىڭ نۆۋەتتىكى نەشرىنى GitHub دىن چۈشۈرەلەيسىز. Apertium نى ھەرخىل مەشغۇلات سۇپىلىرىغا قاچىلاش قوللانمىسىنى ۋىكىنىڭ قاچىلاش بېتى تەمىنلەندى.",
68 | "Contact_Para": " IRC قانىلى بىز بىلەن ئالاقە قىلىشنىڭ ئەڭ تېز ئۇسۇلى بولسا IRC قانىلىمىزغا (apertium# at irc.oftc.net) قاتنىشىش. بۇ ئاچقۇچىلار بىلەن ئىشلەتكۈچىلەر ئۇچرىشىدىغان كۆڭۈلدىكىدەك ئورۇن بولۇپ، قاتنىشىش ئۈچۈن IRC پروگراممىسى زۆرۈر بولمايدۇ؛ پەقەت OFTC webchat نى ئىشلەتسىڭىزلا كۇپايە.
ئېلخەت توپى يەنە، apertium-stuff mailing list ئېلخەت توپىغا تىزىملىتىڭ. شۇندىلا بىر قەدەر ئۇزۇن يازمىلارنى يازالايسىز ۋە توپتىكى ئەزالارنىڭ مۇنازېرىسىگە قاتنىشالايسىز.
ئالاقىلىشىڭ ئەگەر بىرەر خاتالىق بايقىسىڭىز apertium-stuff mailing list ئارقىلىق بىز بىلەن خالىغان ۋاقىتتا ئالاقىلىشىڭ.
"
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/navbar/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { MemoryHistory, MemoryHistoryBuildOptions, createMemoryHistory } from 'history';
3 | import { getAllByRole, getByText, queryAllByRole, render, screen, within } from '@testing-library/react';
4 | import { Router } from 'react-router-dom';
5 | import userEvent from '@testing-library/user-event';
6 |
7 | import { Config as ConfigType, Mode } from '../../../types';
8 | import Config from '../../../../config';
9 | import { ConfigContext } from '../../../context';
10 | import Navbar from '..';
11 |
12 | const renderNavbar = (options?: MemoryHistoryBuildOptions, config: Partial = {}): MemoryHistory => {
13 | const history = createMemoryHistory(options);
14 | const setLocale = jest.fn();
15 |
16 | render(
17 |
18 |
19 |
20 |
21 | ,
22 | );
23 |
24 | return history;
25 | };
26 |
27 | const withinMobileNavbar = () => within(screen.getByTestId('navbar-mobile'));
28 |
29 | describe('navigation options', () => {
30 | it('includes links per mode', () => {
31 | renderNavbar();
32 |
33 | const navbar = screen.getByTestId('navbar-mobile');
34 | const links = getAllByRole(navbar, 'link', { name: (n) => n !== 'Toggle navigation' });
35 |
36 | expect(links).toHaveLength(4);
37 | });
38 |
39 | it('includes button', () => {
40 | renderNavbar();
41 |
42 | const navbar = screen.getByTestId('navbar-mobile');
43 | const buttons = queryAllByRole(navbar, 'button');
44 |
45 | expect(buttons).toHaveLength(0);
46 | });
47 |
48 | describe('with only one mode enabled', () => {
49 | it('hides links', () => {
50 | renderNavbar(undefined, { enabledModes: new Set([Mode.Translation]) });
51 |
52 | const navbar = screen.getByRole('navigation');
53 | const links = queryAllByRole(navbar, 'link');
54 |
55 | expect(links).toHaveLength(0);
56 | });
57 |
58 | it('hides button', () => {
59 | renderNavbar(undefined, { enabledModes: new Set([Mode.Translation]) });
60 |
61 | const navbar = screen.getByRole('navigation');
62 | const buttons = queryAllByRole(navbar, 'button');
63 |
64 | expect(buttons).toHaveLength(0);
65 | });
66 | });
67 | });
68 |
69 | it('defaults to translation', () => {
70 | renderNavbar();
71 |
72 | const navbar = screen.getByTestId('navbar-mobile');
73 | expect(getByText(navbar, 'Translation-Default').className).toContain('active');
74 | });
75 |
76 | describe('subtitle', () => {
77 | const subtitle = 'I am a subtitle';
78 |
79 | it('renders text', () => {
80 | renderNavbar(undefined, { subtitle });
81 | expect(screen.getByText(subtitle)).toBeDefined();
82 | });
83 |
84 | it('renders with color', () => {
85 | const subtitleColor = 'green';
86 | renderNavbar(undefined, { subtitle, subtitleColor });
87 | expect(screen.getByText(subtitle).style.color).toBe('green');
88 | });
89 |
90 | it('renders without color', () => {
91 | renderNavbar(undefined, { subtitle, subtitleColor: undefined });
92 | expect(screen.getByText(subtitle).style.color).toBe('');
93 | });
94 | });
95 |
96 | describe('translation navigation', () => {
97 | it.each(['/translation', '/webpageTranslation', '/docTranslation'])('shows active link for %s', (pathname) => {
98 | renderNavbar({ initialEntries: [pathname] });
99 | expect(withinMobileNavbar().getByText('Translation-Default').className).toContain('active');
100 | });
101 |
102 | it('navigates on button click', () => {
103 | const history = renderNavbar();
104 | userEvent.click(withinMobileNavbar().getByText('Translation-Default'));
105 | expect(history.location.pathname).toEqual('/translation');
106 | });
107 | });
108 |
109 | describe('analysis navigation', () => {
110 | it('shows an active link', () => {
111 | renderNavbar({ initialEntries: ['/analysis'] });
112 | expect(withinMobileNavbar().getByText('Morphological_Analysis-Default').className).toContain('active');
113 | });
114 |
115 | it('navigates on button click', () => {
116 | const history = renderNavbar();
117 | userEvent.click(withinMobileNavbar().getByText('Morphological_Analysis-Default'));
118 | expect(history.location.pathname).toEqual('/analysis');
119 | });
120 | });
121 |
122 | describe('generation navigation', () => {
123 | it('shows an active link', () => {
124 | renderNavbar({ initialEntries: ['/generation'] });
125 | expect(withinMobileNavbar().getByText('Morphological_Generation-Default').className).toContain('active');
126 | });
127 |
128 | it('navigates on button click', () => {
129 | const history = renderNavbar();
130 | userEvent.click(withinMobileNavbar().getByText('Morphological_Generation-Default'));
131 | expect(history.location.pathname).toEqual('/generation');
132 | });
133 | });
134 |
135 | describe('sandbox navigation', () => {
136 | it('shows an active link', () => {
137 | renderNavbar({ initialEntries: ['/sandbox'] });
138 | expect(withinMobileNavbar().getByText('APy_Sandbox-Default').className).toContain('active');
139 | });
140 |
141 | it('navigates on button click', () => {
142 | const history = renderNavbar();
143 | userEvent.click(withinMobileNavbar().getByText('APy_Sandbox-Default'));
144 | expect(history.location.pathname).toEqual('/sandbox');
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/src/strings/rus.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "selimcan",
5 | "solinus",
6 | "jonorthwash",
7 | "vmax"
8 | ],
9 | "last-updated": "2015-02-03",
10 | "locale": [
11 | "ru",
12 | "rus"
13 | ],
14 | "completion": " 79% 80.80%",
15 | "missing": [
16 | "Apertium_Documentation",
17 | "Install_Apertium",
18 | "Install_Apertium_Para",
19 | "Mark_Unknown_Words",
20 | "More_Languages",
21 | "Morphological_Analysis_Help",
22 | "Morphological_Generation_Help",
23 | "Multi_Step_Translation",
24 | "Norm_Preferences",
25 | "Supported_Formats",
26 | "Translate_Webpage",
27 | "Translation_Help"
28 | ]
29 | },
30 | "title": "Апертиум | Открытая платформа машинного перевода",
31 | "tagline": "Открытая платформа машинного перевода",
32 | "description": "Apertium — это платформа машинного перевода. Это свободное программное обеспечение, доступное под лицензией GNU General Public License.",
33 | "Translation": "Перевод",
34 | "Translate": "Перевести",
35 | "Detect_Language": "Определить язык",
36 | "detected": "определен автоматически",
37 | "Instant_Translation": "Мгновенный перевод",
38 | "Translate_Document": "Перевести документ",
39 | "Drop_Document": "Перетащите документ сюда",
40 | "Not_Available": "Перевод ещё не доступен!",
41 | "File_Too_Large": "Файл слишком большой!",
42 | "Cancel": "Отмена",
43 | "Format_Not_Supported": "Формат не поддерживается!",
44 | "Morphological_Analysis": "Морфологический анализ",
45 | "Analyze": "Анализировать",
46 | "Morphological_Generation": "Морфологический синтез",
47 | "Generate": "Синтезировать",
48 | "Spell_Checker": "Проверка правописания",
49 | "APy_Sandbox": "APy Песочница",
50 | "APy_Sandbox_Help": "Отправить любые запросы",
51 | "APy_Request": "Запрос APy",
52 | "Request": "Запросить",
53 | "Language": "Язык",
54 | "Input_Text": "Вводимый текст",
55 | "Notice_Mistake": "Заметили ошибку?",
56 | "Help_Improve": "Помогите нам улучшить Апертиум!",
57 | "Contact_Us": "Если у Вас есть пожелания, Вы заметили ошибку или хотите помочь, напишите нам.",
58 | "Maintainer": "Этот сайт поддерживается и управляется {{maintainer}}.",
59 | "About_Title": "О сайте",
60 | "Enable_JS_Warning": "Этот сайт работает только с включённым JavaScript. Если Вы не можете включить Javascript , воспользуйтесь переводчиками на Prompsit .",
61 | "Not_Found_Error": "404: Извините, эта страница не найдена!",
62 | "About": "Об Apertium",
63 | "Download": "Загрузки",
64 | "Contact": "Связаться с разработчиками",
65 | "Documentation": "Документация",
66 | "About_Apertium": "Об Apertium",
67 | "What_Is_Apertium": "Apertium — это открытая платформа машинного перевода . Изначально она была создана для связанных языковых пар, но теперь поддерживает даже такие необычные пары, как английский—каталанский. Платформа предоставляет
независимый от языка машинный переводчик; инструменты для сбора лингвистических данных для машинного перевода; лингвистические данные для большого количества языковых пар. Apertium приветствует новых разработчиков: если Вы можете улучшить движок машинного перевода или инструменты или собрать лингвистические данные для нас, то свяжитесь с нами .
",
68 | "Documentation_Para": "Документация расположена в нашей вики на подстранице Документация . Мы публикуем разнообразные бумаги с конференций и статьи из журналов, список которых доступен на странице Публикации .",
69 | "Apertium_Downloads": "Загрузки Apertium",
70 | "Downloads_Para": "Текущая версия инструментария Apertium и данные о языковых парах доступны на странице GitHub . Инструкции по установке Apertium на всех основных платформах доступны в вики .",
71 | "Contact_Para": "Канал IRC Самый быстрый способ связаться с нами — это использовать наш IRC канал, #apertium на сервере irc.oftc.net, где общаются пользователи и разработчики. Вам не нужен IRC-клиент, можно использовать вебчат OFTC .
Список рассылки Подпишитесь на наш список рассылки (apertium-stuff) . Туда Вы можете писать предложения или ошибки, или просто обсуждать Apertium.
Ещё один список рассылки С нами можно связаться через список рассылки apertium-contact в случае, если Вы нашли ошибку, хотите, чтобы мы поработали над каким-то проектом или хотите помочь.
"
72 | }
73 |
--------------------------------------------------------------------------------
/src/strings/oci.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Cédric VALMARY"
5 | ],
6 | "last-updated": "2014-05-17",
7 | "locale": [
8 | "oc",
9 | "oci"
10 | ],
11 | "completion": " 66% 82.29%",
12 | "missing": [
13 | "Apertium_Documentation",
14 | "Cancel",
15 | "Drop_Document",
16 | "File_Too_Large",
17 | "Format_Not_Supported",
18 | "Install_Apertium",
19 | "Install_Apertium_Para",
20 | "Mark_Unknown_Words",
21 | "More_Languages",
22 | "Morphological_Analysis_Help",
23 | "Morphological_Generation_Help",
24 | "Multi_Step_Translation",
25 | "Norm_Preferences",
26 | "Not_Found_Error",
27 | "Supported_Formats",
28 | "Translate_Document",
29 | "Translate_Webpage",
30 | "Translation_Help",
31 | "description"
32 | ]
33 | },
34 | "title": "Apertium | Una plataforma liura/open source de traduccion automatica",
35 | "tagline": "Una plataforma liura/open source de traduccion automatica",
36 | "Translation": "Traduccion",
37 | "Translate": "Tradusir",
38 | "Detect_Language": "Detectar la lenga",
39 | "detected": "detectat",
40 | "Instant_Translation": "Traduccion instantanèa",
41 | "Not_Available": "Traduccion pas encara disponibla !",
42 | "Morphological_Analysis": "Analisi morfologica",
43 | "Analyze": "Analisar",
44 | "Morphological_Generation": "Generacion morfologica",
45 | "Generate": "Generar",
46 | "Spell_Checker": "Corrector ortografic",
47 | "APy_Sandbox": "Nauc de sabla APy",
48 | "APy_Sandbox_Help": "Mandatz una requèsta arbitrària",
49 | "APy_Request": "Requèsta APy",
50 | "Request": "Requèsta",
51 | "Language": "Lenga",
52 | "Input_Text": "Tèxte d'entrada",
53 | "Notice_Mistake": "Avètz trobat una error?",
54 | "Help_Improve": "Ajudatz-nos a melhorar Apertium!",
55 | "Contact_Us": "Contactatz-nos se trobatz una error, se vesètz un projècte sul qual vos agradariá‚ qu'i trabalhèssem dessús, o se nos volètz balhar un còp de man.",
56 | "Maintainer": "Site mantengut per {{maintainer}}.",
57 | "About_Title": "A prepaus del site internet",
58 | "Enable_JS_Warning": "Lo site fonciona pas qu'amb Javascript d'activat, se podètz pas activar Javascript , ensajatz las aisinas de traduccions de Prompsit ",
59 | "About": "A prepaus",
60 | "Download": "Telecargar",
61 | "Contact": "Contacte",
62 | "Documentation": "Documentacion",
63 | "About_Apertium": "A prepaus d'Apertium",
64 | "What_Is_Apertium": "Apertium es una plataforma de traduccion automatica liura/open source , concebuda a l'origina per de paras de lengas pròchas, puèi s'es espandit a de paras de lengas mai aluenhadas coma la para anglés-catalan. La plataforma provesís :
un motor de traduccion automatica que depend pas d'una lenga quina que siá d'aisinas lingüisticas per gerir las donadas necessària a la realizacion d'un sistèma de traduccion automatica per una para de lengas donada de donadas lingüisticas per un nombre creissent de paras de lengas. Apertium presa l'arribada de novèls desvolopaires : se estimatz que podètz melhoratz lo motor, las aisinas o las donadas lingüisticas, esitetz pas a prene contacte amb nosautres .
",
65 | "Documentation_Para": "La documentacion se tròba sul Wiki dins la seccion Documentacion . Avèm publicat divèrses articles de conferéncias e de revistas, trobaretz una lista sul Wiki dins la seccion Publications .",
66 | "Apertium_Downloads": "Telecargaments Apertium",
67 | "Downloads_Para": "Las darrièras versions de la bóstia d'aisinas Apertium e tanben las donadas per las paras de lenga son disponiblas sul site de GitHub . Las instruccions per installar Apertium sus las principalas plataformas se tròban sus la pagina d'installacion del Wiki ",
68 | "Contact_Para": "Lo canal IRC Lo biais lo mai rapid per nos contactar. Per aquò far, anatz sus IRC nòstre canal #apertium sul servidor irc.oftc.net, ont trobaretz los utilizaires e desvolopaires d'Apertium.
La lista de difusion Inscrivètz-vos a la lista de difusion d'Apertium , ont podètz postar de messatges mai longs que contenon de suggestions o que tractan de problèmas e tanben discutir d'Apertium d'un biais general.
Contacte Nos podètz contactar via la lista de difusion d'Apertium se trobatz una error, se vesètz un projècte sul qual vos agradariá que trabalhèssem o se nos volètz donar un còp de man.
"
69 | }
70 |
--------------------------------------------------------------------------------
/src/strings/swe.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Per Tunedal"
5 | ],
6 | "last-updated": "2015-11-23",
7 | "locale": [
8 | "swe"
9 | ],
10 | "completion": " 79% 84.74%",
11 | "missing": [
12 | "Apertium_Documentation",
13 | "Install_Apertium",
14 | "Install_Apertium_Para",
15 | "Mark_Unknown_Words",
16 | "More_Languages",
17 | "Morphological_Analysis_Help",
18 | "Morphological_Generation_Help",
19 | "Multi_Step_Translation",
20 | "Norm_Preferences",
21 | "Supported_Formats",
22 | "Translate_Webpage",
23 | "Translation_Help"
24 | ]
25 | },
26 | "title": "Apertium | En fri maskinöversättningsplattform i öppen källkod",
27 | "tagline": "En fri maskinöversättningsplattform i öppen källkod",
28 | "description": "Apertium är en regelbaserad maskinöversättningsplatform. It is free software and released under the terms of the GNU General Public License. Det är fri mjukvara som är utgiven under villkoren i GNU General Public License",
29 | "Translation": "Översättning",
30 | "Translate": "Översätt",
31 | "Detect_Language": "Detektera språk",
32 | "detected": "detekterat",
33 | "Instant_Translation": "Omedelbar översättning",
34 | "Translate_Document": "Översätt ett dokument",
35 | "Drop_Document": "Släpp ett dokument",
36 | "Not_Available": "Översättning ej ännu tillgänglig!",
37 | "File_Too_Large": "Filen är för stor!",
38 | "Cancel": "Avbryt",
39 | "Format_Not_Supported": "Formatet stöds inte!",
40 | "Morphological_Analysis": "Morfologisk analys",
41 | "Analyze": "Analys",
42 | "Morphological_Generation": "Morfologisk generering",
43 | "Generate": "Generera",
44 | "Spell_Checker": "Stavningskontroll",
45 | "APy_Sandbox": "APy sandlåda",
46 | "APy_Sandbox_Help": "Sänd en godtycklig begäran",
47 | "APy_Request": "APy Förfråga",
48 | "Request": "Förfrågan",
49 | "Language": "Språk",
50 | "Input_Text": "Inmatad text",
51 | "Notice_Mistake": "Har du hittat ett misstag?",
52 | "Help_Improve": "Hjälp oss förbättra Apertium!",
53 | "Contact_Us": "Kontakta oss gärna om du hittar ett misstag, om det finns ett projekt du gärna vill att vi arbetar med eller om du skulle vilja hjälpa till.",
54 | "Maintainer": "Den här webbplatsen underhålls av {{maintainer}}.",
55 | "About_Title": "Om webbplatsen",
56 | "Enable_JS_Warning": "Denna sajt fungerar bara om du har aktiverat JavaScript, om du inte kan aktivera Javascript , pröva då översättarna på Prompsit .",
57 | "Not_Found_Error": "404 FEL: Ursäkta, tyvärr finns inte sidan längre!",
58 | "About": "Om",
59 | "Download": "Nedladdning",
60 | "Contact": "Kontakt",
61 | "Documentation": "Dokumentation",
62 | "About_Apertium": "Om Apertium",
63 | "What_Is_Apertium": "Apertium är en En fri maskinöversättningsplattform i öppen källkod , ursprungligen ägnad till besläktade språkpar, men utvidgad till att hantera mer olikartade språkpar (som Engelska-Katalanska). Plattformen tillhandahåller
en språkoberoende maskinöversättningsmotor verktyg för att hantera de linguistiska data som behövs för ett visst språkpar och Linguistiska data för ett växande antal språkpar. Apertium välkomnar nya utvecklare: om du tror att du kan förbättra motorn eller verktygen, eller utveckla linguistiska data åt oss, tveka inte attkontakta oss .
",
64 | "Documentation_Para": "Dokumentation hittar du på vår Wiki under Documentation undersida. Vi har publicerat diverse konferensbidrag och tidningsartiklar, du hittar en lista på dem på Wikin under Publications .",
65 | "Apertium_Downloads": "Apertium Nedladdningar",
66 | "Downloads_Para": "Aktuell version av Apertium toolbox liksom av språkparsdata är tillgängliga på GitHub-sidan . Installationsinstruktioner för Apertium på alla större plattformar tillhandahålls på Wikins Installation-sida .",
67 | "Contact_Para": "IRC-kanalen Det snabbaste sättet att kontakta oss är att ansluta sig till IRC -kanal, #apertium på irc.oftc.net, där användare och utvecklare av Apertium träffas. Du behöver inte någon IRC-klient; du kan använda OFTC webchat .
E-postlista Prenumerera också på e-postlistan apertium-stuff , där du kan publicera längre förslag eller problem, liksom följa allmänna Apertium-diskussioner.
Kontakt Kontakta oss gärna på e-postlistan apertium-contact om du hittar ett misstag, du vill att vi tar oss an ett projekt, eller om du skulle vilja hjälpa till.
"
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/navbar/index.tsx:
--------------------------------------------------------------------------------
1 | import './navbar.css';
2 |
3 | import * as React from 'react';
4 | import Nav, { NavProps } from 'react-bootstrap/Nav';
5 | import BootstrapNavbar from 'react-bootstrap/Navbar';
6 | import Container from 'react-bootstrap/Container';
7 | import { LinkContainer } from 'react-router-bootstrap';
8 |
9 | import { ConfigContext } from '../../context';
10 | import { Path as DocTranslationPath } from '../translator/DocTranslationForm';
11 | import LocaleSelector from '../LocaleSelector';
12 | import { Mode } from '../../types';
13 | import { Path as TextTranslationPath } from '../translator/TextTranslationForm';
14 | import { Path as WebpageTranslationPath } from '../translator/WebpageTranslationForm';
15 | import logo from './Apertium_box_white_small.embed.png';
16 | import { useLocalization } from '../../util/localization';
17 |
18 | const Logo = (): React.ReactElement => (
19 |
32 | );
33 |
34 | const TagLine = (): React.ReactElement => {
35 | const { t } = useLocalization();
36 |
37 | return (
38 |
46 | {t('tagline')}
47 |
48 | );
49 | };
50 |
51 | const NavbarNav: React.ComponentType = (props: NavProps) => {
52 | const { t } = useLocalization();
53 | const { enabledModes, defaultMode } = React.useContext(ConfigContext);
54 |
55 | if (enabledModes.size === 1) {
56 | return null;
57 | }
58 |
59 | return (
60 |
61 | {enabledModes.has(Mode.Translation) && (
62 |
63 |
65 | [TextTranslationPath, WebpageTranslationPath, DocTranslationPath].includes(pathname) ||
66 | (pathname === '/' && defaultMode === Mode.Translation)
67 | }
68 | to={TextTranslationPath}
69 | >
70 | {t('Translation')}
71 |
72 |
73 | )}
74 | {enabledModes.has(Mode.Analysis) && (
75 |
76 |
78 | pathname === '/analysis' || (pathname === '/' && defaultMode === Mode.Analysis)
79 | }
80 | to={'/analysis'}
81 | >
82 | {t('Morphological_Analysis')}
83 |
84 |
85 | )}
86 | {enabledModes.has(Mode.Generation) && (
87 |
88 |
90 | pathname === '/generation' || (pathname === '/' && defaultMode === Mode.Generation)
91 | }
92 | to={'/generation'}
93 | >
94 | {t('Morphological_Generation')}
95 |
96 |
97 | )}
98 | {enabledModes.has(Mode.Sandbox) && (
99 |
100 |
102 | pathname === '/sandbox' || (pathname === '/' && defaultMode === Mode.Sandbox)
103 | }
104 | to={'/sandbox'}
105 | >
106 | {t('APy_Sandbox')}
107 |
108 |
109 | )}
110 |
111 | );
112 | };
113 |
114 | const Navbar = ({ setLocale }: { setLocale: React.Dispatch> }): React.ReactElement => {
115 | const { subtitle, subtitleColor, enabledModes } = React.useContext(ConfigContext);
116 |
117 | return (
118 |
119 |
120 |
121 |
122 |
123 |
124 | {subtitle && (
125 |
126 | {subtitle}
127 |
128 | )}
129 | Apertium
130 |
131 |
132 |
133 | {enabledModes.size > 1 && }
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | );
149 | };
150 |
151 | export default Navbar;
152 |
--------------------------------------------------------------------------------
/src/strings/srd.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [],
4 | "last-updated": "",
5 | "locale": [
6 | "srd"
7 | ],
8 | "completion": " 80% 89.80%",
9 | "missing": [
10 | "Apertium_Documentation",
11 | "Install_Apertium",
12 | "Install_Apertium_Para",
13 | "More_Languages",
14 | "Morphological_Analysis_Help",
15 | "Morphological_Generation_Help",
16 | "Multi_Step_Translation",
17 | "Norm_Preferences",
18 | "Supported_Formats",
19 | "Translate_Webpage",
20 | "Translation_Help"
21 | ]
22 | },
23 | "title": "Apertium | Una prataforma lìbera a còdighe abertu pro sa tradutzione automàtica",
24 | "tagline": "Una prataforma lìbera a còdighe abertu pro sa tradutzione automàtica",
25 | "description": "Apertium est una prataforma de tradutzione automàtica basada subra de règulas. Est unu programa lìberu publicadu suta sas cunditziones de sa GNU General Public License.",
26 | "Translation": "Tradutzione",
27 | "Translate": "Borta",
28 | "Detect_Language": "Rileva sa limba in manera automàtica",
29 | "detected": "rilevada",
30 | "Instant_Translation": "Tradutzione istantànea",
31 | "Mark_Unknown_Words": "Sinnala sas paràulas disconnotas",
32 | "Translate_Document": "Borta unu documentu",
33 | "Drop_Document": "Traga unu documentu",
34 | "Not_Available": "Sa tradutzione no est galu a disponimentu!",
35 | "File_Too_Large": "S'archìviu est tropu mannu!",
36 | "Cancel": "Cantzella",
37 | "Format_Not_Supported": "Su formadu no est suportadu!",
38 | "Morphological_Analysis": "Anàlisi morfològica",
39 | "Analyze": "Analiza",
40 | "Morphological_Generation": "Generatzione morfològica",
41 | "Generate": "Gènera",
42 | "Spell_Checker": "Curretore ortogràficu",
43 | "APy_Sandbox": "APy sandbox",
44 | "APy_Sandbox_Help": "Imbia una rechesta cale si siat",
45 | "APy_Request": "Rechesta de APy",
46 | "Request": "Rechesta",
47 | "Language": "Limba",
48 | "Input_Text": "Testu de intrada",
49 | "Notice_Mistake": "Ais agatadu una faddina?",
50 | "Help_Improve": "Agiuade·nos a megiorare Apertium!",
51 | "Contact_Us": "Iscriide·nos si agatades carchi faddina, si cherides chi traballemus a carchi progetu o si cherides collaborare.",
52 | "Maintainer": "Custu situ web est mantènnidu dae {{maintainer}}.",
53 | "About_Title": "Subra de custu situ",
54 | "Enable_JS_Warning": "Custu situ funtzionat petzi si JavaScript est ativadu, si non podides ativare JavaScript , proade sos tradutores de Prompsit .",
55 | "Not_Found_Error": "404 Error: Custa pàgina no esistet prus!",
56 | "About": "Informatziones",
57 | "Download": "Iscàrriga",
58 | "Contact": "Cuntatade·nos",
59 | "Documentation": "Documentatzione",
60 | "About_Apertium": "Subra de Apertium",
61 | "What_Is_Apertium": "Apertium est una prataforma de tradutzione automàtica lìbera e a còdighe abertu , a su cumintzu creada pro una paja de limbas serentes, e posca ampliada Sa prataforma frunit
unu motore de tradutzione automàtica indipendente dae sa limba ainas pro gestire sos datos linguìsticos e netzessàrios pro fraigare unu sistema de tradutzione pro una cumbinatzione linguìstica dislindada e datos linguìsticos pro unu nùmeru in crèschida de còpias de limbas. Apertium dat su bene bènnidu a isvilupadores noos: si pensades chi si potzat megiorare su motore, sas ainas o chi si potzant isvilupare sos datos linguìsticos cuntatade·nos .
",
62 | "Documentation_Para": "Podides agatare sa documentatzione in sa wiki nostra, in sa pàgina Documentation . Amus publicadu paritzos artìculos e cunferèntzias. Agatades sa lista intrea in sa wiki, in sa pàgina Publications .",
63 | "Apertium_Downloads": "Iscarrigamentos de Apertium",
64 | "Downloads_Para": "Sas versiones atuales de Apertium e sos datos linguìsticos sunt a disponimentu in sa pàgina de GitHub . Sas istrutziones de installatzione de Apertium in sa majoria de sas prataformas sunt a disponimentu in sa pagina Installation de sa wiki .",
65 | "Contact_Para": "Canale IRC Sa manera prus lestra de nos cuntatare est intrende a su canale IRC nostru, #apertium in irc.oftc.net, in ue bi sunt sos utentes e sos isvilupadores de Apertium. Si non tenides perunu cliente IRC; podides impreare su webchat de OFTC .
Lista de messàgios Podides fintzas aderire a sa lista de messàgios apertium-stuff , in ue podides iscriere propostas, chistionare de problemas e sighire chistiones generales de Apertium.
Cuntatu Cuntatade·nos tràmite sa lista de messàgios apertium-contact si agatades carchi faddina, si cherides chi traballemus a carchi progetu o si cherides collaborare.
"
66 | }
67 |
--------------------------------------------------------------------------------
/src/strings/ara.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "emascandam"
5 | ],
6 | "last-updated": "2019-04-06",
7 | "locale": [
8 | "ar",
9 | "ara"
10 | ],
11 | "completion": " 95% 89.08%",
12 | "missing": [
13 | "Apertium_Documentation",
14 | "More_Languages",
15 | "Norm_Preferences"
16 | ]
17 | },
18 | "title": " أبرتيُم | منصة ترجمة إلكترونية حرة ومفتوحة المصدر",
19 | "tagline": "منصََّة ترجمة إلكترونيَّة حُرَّة ومفتوحة المصدر",
20 | "description": "أبرتيُم هي منصة ترجمة إلكترونية تعنمد على القوانين اللغوية المُعدَّة مًسبقًا. أبرتيُم برمجية حُرَّة مُتاحة تحت رخصة غنو العامة..",
21 | "Translation": "ترجمة",
22 | "Translation_Help": "أدخل نصًّا أو رابطًا للترجمة.",
23 | "Translate": "ترجِم",
24 | "Detect_Language": "تحديد اللغة",
25 | "detected": "تم التحديد",
26 | "Instant_Translation": "ترجمة فورية",
27 | "Mark_Unknown_Words": "تحديد الكلمات المجهولة",
28 | "Multi_Step_Translation": "ترجمة متعددة الخطوات (تجريبي)",
29 | "Translate_Document": "ترجمة مستند",
30 | "Drop_Document": "ارمي المستند",
31 | "Not_Available": "الترجمة غير متوفرة لحد الآن!",
32 | "File_Too_Large": "الملف كبير جدًّا!",
33 | "Cancel": "إلغاء",
34 | "Format_Not_Supported": "الصيغة غير مدعومة",
35 | "Translate_Webpage": "ترجمة صفحة إنترنت",
36 | "Supported_Formats": "الصيغ المدعومة: .txt, .rtf, .odp, .ods, .odt (ليبره أوفس/أوبن أوفس), .xlsx, .pptx, .docx (مايكروسوفت أوفس 2003 وأحدث). ملفات وورد 97 وملفات الـPDF غير مدعومة. ",
37 | "Morphological_Analysis": "المحلل المورفولوجي",
38 | "Morphological_Analysis_Help": "أدخل(ي) نصًّا للمحلل المورفولوجي",
39 | "Analyze": "تحليل",
40 | "Morphological_Generation": "توليد مورفولوجي",
41 | "Morphological_Generation_Help": "أدخل الهيئات المورفولوجية التي تريد توليدها نصًّا.",
42 | "Generate": "توليد",
43 | "Spell_Checker": "المدقق الإملائي",
44 | "APy_Sandbox": "APy ساندبوكس",
45 | "APy_Sandbox_Help": "إرسال طلبات اعتباطية",
46 | "APy_Request": "APy طلب",
47 | "Request": "الطلب",
48 | "Language": "اللغة",
49 | "Input_Text": "النص",
50 | "Notice_Mistake": "وجدت(ي) خطأً؟",
51 | "Help_Improve": "ساعد(ي) في تحسين أبرتيُم!",
52 | "Contact_Us": "نرحب بتواصلك معنا في حالة العثور على خطأ، أو إن كان هناك مشروع تود(ين) أن تطرح(ي) علينا العمل عليه، أو إن أردتم المساعدة.",
53 | "Maintainer": "هذا الموقع يتكفل بصيانته {{maintainer}}.",
54 | "About_Title": "عن الموقع",
55 | "Enable_JS_Warning": "هذا الموقع لا يعمل إلَّا في حال تفعيل جافاسكربت، إن لم تكُن بالاستطاعة تفعيل جافاسكربت , يُمكن تجريب المترجمات الموجودة على Prompsit .",
56 | "Not_Found_Error": "خطأ 404: عذرًا، هذه الصفحة لم تعُد موجودة!",
57 | "About": "عن",
58 | "Download": "تحميل",
59 | "Contact": "تواصُل",
60 | "Documentation": "التوثيق",
61 | "About_Apertium": "حول أبرتُم",
62 | "What_Is_Apertium": "أبرتيُم هي منصة ترجمة إلكترونية حرة ومفتوحة المصدر/b> بدأت كمنصة ترجمة إلكترونية للغات المتقاربة وتطورت لاحقا لتشمل لغات متباعدة أكثر (كالإنجليزية-الكتلانية). المنصة توفر
محرك ترجمة إلكترونية مستقل أدوات لإدارة البيانات اللغوية الضرورية لإنشاء منصة ترجمة إلكترونية بين لغتين، و البيانات اللغوية لعدد متزايد من الأقران اللغوية. أبرتيُم ترحب بالمطورين الجُدد: إن كنت ترى أن بإمكانك تطوير المحرك أو الأدوات، أو إنشاء وتطوير البيانات اللغوية للمنصة، فلا تتردد بتواصلوا معنا .
",
63 | "Documentation_Para": "التوثيقات الخاصة بالمشروع يُمكن إيجادها في الويكي تحت صفحة التوثيق الفرعية. نشرنا عددًا كبيرًا من المقالات العلملية وسوابق المؤتمرات، يُمكن إيجادها تحت صفحة المنشورات .",
64 | "Apertium_Downloads": "تحميلات أبرتيمُ",
65 | "Downloads_Para": "الإصدارات الحالية من صندوق أدوات أبرتيُم (Apertium toolbox) وكذلك بيانات الأقران اللغوية متوفرة من صفحة الـGitHub . تعليمات التثبيت لجميع المنصات الرئيسية متوفر في صفحة التثبيت في الويكي .",
66 | "Contact_Para": "IRC قناة أسرع طريقة للتواصل معنا هي عن طريف قناة الـ IRC الخاصة بالمشروع, #apertium على irc.oftc.net, حيث يلتقي المستخدمين والمطورين. لا حاجة لبرامج خاصَّة؛ يمكنك استخدام OFTC للويب .
القائمة البريدية أيضًا، اشترك مع قائمة أبرتيُم البريدية (apertium-stuff) ، حيث يمكنك مشاركة القضايا والمشاكل بشكل مفصل أكثر، وأيضًا للاطلاع على آخر مستجدات وأخبار أبرتيُم.
تواصل لا تتردد(ي) بالتواصل معنا عبر قائمة apertium-contact البريدية إن وجدت(ي) خطأً، أو إن كان هناك مشروع تود(ين) منا العمل عليه، أو إن أردتم المساعدة بشكل عام..
",
67 | "Install_Apertium": "تثبيت أبرتيُم",
68 | "Install_Apertium_Para": "هل الاستجابة بطيئة؟ قد تكون سرفراتنا مشغولة. تعلم(ي) كيفية تنصيب أبرتيُم محليًّا ."
69 | }
70 |
--------------------------------------------------------------------------------
/src/strings/uzb.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "tarjimon"
5 | ],
6 | "last-updated": "2015-17-07",
7 | "locale": [
8 | "uz",
9 | "uzb"
10 | ],
11 | "completion": " 79% 90.20%",
12 | "missing": [
13 | "Apertium_Documentation",
14 | "Install_Apertium",
15 | "Install_Apertium_Para",
16 | "Mark_Unknown_Words",
17 | "More_Languages",
18 | "Morphological_Analysis_Help",
19 | "Morphological_Generation_Help",
20 | "Multi_Step_Translation",
21 | "Norm_Preferences",
22 | "Supported_Formats",
23 | "Translate_Webpage",
24 | "Translation_Help"
25 | ]
26 | },
27 | "title": "Apertium | Tekin, ochiq kodli mashina tarjima platformasi",
28 | "tagline": "Tekin, ochiq kodli mashina tarjima platformasi",
29 | "description": "Apertium - qoidaga asoslangan mashina tarjima platformasi. Bu dastur tekin bo‘lib GNU General Public License shartlari asosida chop etilgan.",
30 | "Translation": "Tarjima",
31 | "Translate": "Tarjima qilish",
32 | "Detect_Language": "Tilni aniqlash",
33 | "detected": "aniqlandi",
34 | "Instant_Translation": "Tez tarjima",
35 | "Translate_Document": "Hujjatni tarjima qilish",
36 | "Drop_Document": "Hujjatni tashlash",
37 | "Not_Available": "Tarjima hali mavjud emas!",
38 | "File_Too_Large": "Fayl juda katta!",
39 | "Cancel": "Bekor qilish",
40 | "Format_Not_Supported": "Format qo‘llanilmaydi!",
41 | "Morphological_Analysis": "Morfologik tahlil",
42 | "Analyze": "Tahlil qilish",
43 | "Morphological_Generation": "Morfologik generator",
44 | "Generate": "Ishlab chiqarish",
45 | "Spell_Checker": "Imlo tekshiruvchi",
46 | "APy_Sandbox": "APy qumdon",
47 | "APy_Sandbox_Help": "Ixtiyoriy so‘rovlarni jo‘natish",
48 | "APy_Request": "APy So‘rov",
49 | "Request": "So‘rov",
50 | "Language": "Til",
51 | "Input_Text": "Matn kiritish",
52 | "Notice_Mistake": "Xato topdingizmi?",
53 | "Help_Improve": "Apertiumni yaxshilashga bizga yordam bering!",
54 | "Contact_Us": "Xato topsangiz, bizni biror loyiha ustida ishlashimizni xoxlasangiz, yoki yordam bermoqchi bo‘lsangiz bizga murojaat qilishdan tortinmang.",
55 | "Maintainer": "{{maintainer}} ushbu veb saytni ta‘mirlab turibdi.",
56 | "About_Title": "Ushbu veb sayt haqida",
57 | "Enable_JS_Warning": "Ushbu sayt faqatgina JavaScript faol bo‘lganda ishlaydi; agar JavaScriptni faollashtir a olmasangiz, Prompsitda tarjimonlar ni ishlatib ko‘ring.",
58 | "Not_Found_Error": "404 Xatolik: Uzr, u sahifa boshqa mavjud emas!",
59 | "About": "Haqida",
60 | "Download": "Yuklab olish",
61 | "Contact": "Aloqa qilish",
62 | "Documentation": "Hujjatlar",
63 | "About_Apertium": "Apertium haqida",
64 | "What_Is_Apertium": "Apertium dastlab bog‘liq tillar juftligiga mo‘ljallangan, lekin keyinchalik bir-biridan ayiriluvchi tillar juftligi (mas. Inglizcha - Katalancha) bilan ishlashga moslashtirilgan tekin, ochiq kodli mashina tarjima platformasi dir. Platforma quyidagilarni ta‘minlaydi:
biror bir tilga bog‘liq bo‘lmagan mashina tarjima motori berilgan til juftligi uchun mashina tarjimasi tizimini yaratish uchun kerakli lingvistik ma‘lumotlarni boshqaruvchi asboblar va oshib borayotgan tillar juftligi uchun lingvistik ma‘lumotlar. Apertium yangi tuzuvchilarni xush ko‘radi: agar motorni yoki asboblarni yaxshilay olaman deb o‘ylasangiz, yoki biz uchun lingvistik ma‘lumotlarni rivojlantirib bilsangiz, bizga murojaat qilish dan tortinmang.
",
65 | "Documentation_Para": "Hujjatlarni Viki mizning Hujjatlar ost-sahifasida topshingiz mumkin. Biz turli xil konferensiya ilmiy hujjatlari va jurnal maqolalarini chop etganmiz. Ularning ro‘yxatini Vikining Nashrlar sahifasida topishingiz mumkin.",
66 | "Apertium_Downloads": "Apertium Ko‘chirmalari",
67 | "Downloads_Para": "Apertium asboblar qutisi va til juftligi ma‘lumotlari ning ushbu versiyalari GitHub sahifasi da mavjud. Apertiumni barcha muhim platformalarda o‘rnatish ko‘rsatmalari Vikining O‘rnatish sahifasi da berilgan.",
68 | "Contact_Para": "Chat (IRC) kanali irc.oftc.net dagi Apertiumning foydalanuvchilari va tuzuvchilari uchrashadigan #apertium IRC kanalimiz ga ulanish orqali biz bilgan tezda bog‘lanishingiz mumkin. Bu uchun sizga IRC mijozi shart emas; OFTC veb chatini ni ishlatishingiz mumkin.
Tarqatish ro‘yxatlari Uzunroq taklif yoki muammolarni yozishingiz, shu bilan birga umumiy Apertium muhokamalarni kuzatishingiz mumkin bo‘lgan apertium-stuff tarqatish ro‘yxatiga ham a‘zo bo‘ling.
Murojaat qilish Xato topsangiz, bizni biror loyiha ustida ishlashimizni xoxlasangiz, yoki yordam bermoqchi bo‘lsangiz bizga apertium-contact tarqatish ro‘yxati orqali bizga murojaat qilishdan tortinmang
"
69 | }
70 |
--------------------------------------------------------------------------------
/src/strings/fin.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "mie",
5 | "inariksit"
6 | ],
7 | "last-updated": "2014-08-04",
8 | "locale": [
9 | "fi",
10 | "fin"
11 | ],
12 | "completion": " 95% 93.59%",
13 | "missing": [
14 | "Apertium_Documentation",
15 | "More_Languages",
16 | "Norm_Preferences"
17 | ]
18 | },
19 | "title": "Apertium – Vapaa konekäännösalusta",
20 | "tagline": "Vapaa avoimen lähdekoodin konekäännösalusta",
21 | "description": "Apertium on sääntöpohjainen konekäännösalusta. Se on vapaata lähdekoodia ja julkaistu GNU:N GPL-lisenssillä.",
22 | "Translation": "Käännös",
23 | "Translation_Help": "Kirjoita tekstiä tai verkko-osoite käännettäväksi.",
24 | "Translate": "Käännä",
25 | "Detect_Language": "Tunnista kieli",
26 | "detected": "tunnistettu",
27 | "Instant_Translation": "Välitön käännös",
28 | "Mark_Unknown_Words": "Merkkaa tuntemattomat sanat",
29 | "Multi_Step_Translation": "Monivaiheinen käännös (testiversio)",
30 | "Translate_Document": "Käännä dokumentti",
31 | "Drop_Document": "Tiputa dokumentti",
32 | "Not_Available": "Käännöstä ei ole saatavilla.",
33 | "File_Too_Large": "Tiedosto on liian suuri.",
34 | "Cancel": "Peruuta",
35 | "Format_Not_Supported": "Tiedostomuotoa ei tueta.",
36 | "Translate_Webpage": "Käännä sivusto",
37 | "Supported_Formats": "Tuetut tiedostomuodot: .txt, .rtf, .odp, .ods, .odt (LibreOffice/OpenOffice), .xlsx, .pptx, .docx (Microsoft Office 2003 ja uudemmat). Word 97:n .doc-tiedosto ja PDF:t ei toimi. ",
38 | "Morphological_Analysis": "Morfologinen jäsennys",
39 | "Morphological_Analysis_Help": "Syötä tekstiä analysoitavaksi.",
40 | "Analyze": "Analysoi",
41 | "Morphological_Generation": "Morfologinen generointi",
42 | "Morphological_Generation_Help": "Syötä morfologisia muotoja analysoitavaksi.",
43 | "Generate": "Generoi",
44 | "Spell_Checker": "Oikaisuluku",
45 | "APy_Sandbox": "APy-hiekkalaatikko",
46 | "APy_Sandbox_Help": "Lähettele mielivaltaisia pyyntöjä",
47 | "APy_Request": "APy-pyyntöjä",
48 | "Request": "Pyyntö",
49 | "Language": "Kieli",
50 | "Input_Text": "Syöteteksti",
51 | "Notice_Mistake": "Löysitkö virheen?",
52 | "Help_Improve": "Auta Apertiumin kehittämisessä.",
53 | "Contact_Us": "Ota yhteyttä, jos löydät virheitä, tiedät sopivan projektin tai haluat muutoin auttaa.",
54 | "Maintainer": "Tämän sivuston ylläpitäjä on {{maintainer}}.",
55 | "About_Title": "Tietoja sivustosta.",
56 | "Enable_JS_Warning": "Sivuston toiminta vaatii JavaScriptiä. Jollei JavaScriptiä voi käyttää, vaihtoehtoisia kääntimiä löytyyPrompsitin sivustolta .",
57 | "Not_Found_Error": "Hakemaasi sivua ei löytynyt.",
58 | "About": "Tietoja",
59 | "Download": "Lataa",
60 | "Contact": "Yhteystiedot",
61 | "Documentation": "Ohjeet",
62 | "About_Apertium": "Tietoja Apertiumista",
63 | "What_Is_Apertium": "Apertium on vapaa avoimen lähdekoodin konekäännösalusta . Alunperin Apertium tehtiin lähisukukielien käännökseen, mutta sitä on myöhemmin laajennettu etäisemmille kielipareille (esim. englanti-katalaani). Alusta sisältää:
kieliriippumattoman konekäännösenginen työkaluja sellaisen lingvistisen datan hallintaan jota tarvitaan konekäännöksessä, ja lingvististä dataa kasvavalle joukolle kieliä Apertium kannustaa uusien kehittäjien osanottoa, jos voit auttaa enginen tai työkalujen kehityksessä tai kerryttää lingvististä dataa, voit lähettää viestin meille .
",
64 | "Documentation_Para": "Ohjeita löytyy Apertiumin wikin osiosta Documentation . Apertiumista on julkaistu konferenssi- ja journaaliartikkeleita, jotka on luetteloitu wikisivulle Publications.",
65 | "Apertium_Downloads": "Apertiumin ladattavat tiedostot",
66 | "Downloads_Para": "Apertium toolboxin versiot ja kielidata löytyvät Apertiumin GitHub-sivuilta . Asennusohjeet useimmille alustoille löytyvät wikisivulta Installation .",
67 | "Contact_Para": "Irkkikanava Nopein tapa ottaa yhteyttä on IRC -kanava #apertium OFTC-verkossa. Kanavalle pääsyyn ei tarvitse välttämättä irkkisovellusta, OFTCn webchatilla pääsee alkuun.
Postituslista Postituslistalle apertium-stuff kannattaa liittyä. Siellä käydään keskustelua mutkikaammista muutosehdotuksista ja ongelmista sekä yleisistä Apertium-jutuista.
Yhteystiedot Apertium-contact-listalle voi lähettää postia virheistä, ehdottaa projekteja tai tarjota apua.
",
68 | "Install_Apertium": "Asenna Apertium",
69 | "Install_Apertium_Para": "Toimiiko sivusto liian hitaasti? Palvelimet saattavat olla ylikuormitetut. Wikisivuilta voit oppia kuinka apertiumin voi asentaa ."
70 | }
71 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import './bootstrap.css';
2 | import './app.css';
3 | import './rtl.css';
4 |
5 | import * as React from 'react';
6 | import { HashRouter, Route, useHistory, useLocation } from 'react-router-dom';
7 | import { MatomoProvider, createInstance } from '@datapunt/matomo-tracker-react';
8 | import Container from 'react-bootstrap/Container';
9 | import axios from 'axios';
10 |
11 | import { ConfigContext, LocaleContext, StringsContext } from './context';
12 | import { PRELOADED_STRINGS, Strings, tt } from './util/localization';
13 | import { langDirection, toAlpha2Code } from './util/languages';
14 | import { Mode } from './types';
15 |
16 | import Analyzer from './components/Analyzer';
17 | import { Path as DocTranslationPath } from './components/translator/DocTranslationForm';
18 | import Footer from './components/footer';
19 | import Generator from './components/Generator';
20 | import LocaleSelector from './components/LocaleSelector';
21 | import Navbar from './components/navbar';
22 | import Sandbox from './components/Sandbox';
23 | import Translator from './components/translator/Translator';
24 | import { Mode as TranslatorMode } from './components/translator';
25 | import { Path as WebpageTranslationPath } from './components/translator/WebpageTranslationForm';
26 | import WithInstallationAlert from './components/WithInstallationAlert';
27 | import WithLocale from './components/WithLocale';
28 |
29 | const Interfaces = {
30 | [Mode.Translation]: Translator,
31 | [Mode.Analysis]: Analyzer,
32 | [Mode.Generation]: Generator,
33 | [Mode.Sandbox]: Sandbox,
34 | } as Record>;
35 |
36 | const App = ({ setLocale }: { setLocale: React.Dispatch> }): React.ReactElement => {
37 | const history = useHistory();
38 | const location = useLocation();
39 | const locale = React.useContext(LocaleContext);
40 | const { defaultLocale, defaultMode, enabledModes, matomoConfig } = React.useContext(ConfigContext);
41 |
42 | // Fetch strings on locale change.
43 | const [strings, setStrings] = React.useState(PRELOADED_STRINGS);
44 | React.useEffect(() => {
45 | if (locale in strings) {
46 | return;
47 | }
48 |
49 | void (async () => {
50 | let localeStrings: Strings;
51 | try {
52 | localeStrings = (await axios({ url: `strings/${locale}.json`, validateStatus: (status) => status === 200 }))
53 | .data as unknown as Strings;
54 | } catch (error) {
55 | console.warn(`Failed to fetch strings, falling back to default ${defaultLocale}`, error);
56 | return;
57 | }
58 |
59 | setStrings((strings) => ({ ...strings, [locale]: localeStrings }));
60 | })();
61 | }, [defaultLocale, locale, strings]);
62 |
63 | // Update global strings on locale change.
64 | React.useEffect(() => {
65 | const htmlElement = document.getElementsByTagName('html')[0];
66 | htmlElement.dir = langDirection(locale);
67 | htmlElement.lang = toAlpha2Code(locale) || locale;
68 |
69 | (document.getElementById('meta-description') as HTMLMetaElement).content = tt('description', locale, strings);
70 |
71 | document.title = tt('title', locale, strings);
72 | }, [locale, strings]);
73 |
74 | React.useEffect(() => {
75 | const body = document.getElementsByTagName('body')[0];
76 | const handleDragEnter = () => {
77 | if (location.pathname !== DocTranslationPath) {
78 | history.push(DocTranslationPath);
79 | }
80 | };
81 | body.addEventListener('dragenter', handleDragEnter);
82 | return () => body.removeEventListener('dragenter', handleDragEnter);
83 | }, [history, location.pathname]);
84 |
85 | const wrapRef = React.createRef();
86 | const pushRef = React.createRef();
87 |
88 | const matomoInstance = React.useMemo(
89 | () => createInstance(matomoConfig || { disabled: true, urlBase: '-', siteId: 1 }),
90 | [matomoConfig],
91 | );
92 |
93 | React.useEffect(() => matomoInstance.trackPageView(), [matomoInstance]);
94 |
95 | return (
96 |
97 |
98 |
99 |
107 |
108 |
109 | {Object.values(Mode).map(
110 | (mode) =>
111 | enabledModes.has(mode) && (
112 |
118 | ),
119 | )}
120 | {enabledModes.has(Mode.Translation) && (
121 | <>
122 |
123 |
124 |
125 |
126 |
127 |
128 | >
129 | )}
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | );
141 | };
142 |
143 | const ConnectedApp: React.VoidFunctionComponent = () => (
144 |
145 | {(props) => }
146 |
147 | );
148 |
149 | export default ConnectedApp;
150 |
--------------------------------------------------------------------------------
/src/strings/nno.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "unhammer"
5 | ],
6 | "last-updated": "2022-04-09",
7 | "locale": [
8 | "nn",
9 | "nno"
10 | ],
11 | "completion": " 99% 98.57%",
12 | "missing": [
13 | "More_Languages"
14 | ]
15 | },
16 | "title": "Apertium | Ei fri/open kjeldekode maskinomsetjingsplattform",
17 | "tagline": "Ei fri/open kjeldekode maskinomsetjingsplattform",
18 | "description": "Apertium er ei regelbasert maskinomsetjingsplattform, og er fri programvare tilgjengeleg under GNU General Public License.",
19 | "Apertium_Documentation": "Dokumentasjon av Apertium",
20 | "Translation": "Omsetjing",
21 | "Translation_Help": "Skriv inn teksta eller nettadressa du vil omsetja.",
22 | "Translate": "Omset",
23 | "Detect_Language": "Gjenkjenn språk",
24 | "detected": "gjenkjent",
25 | "Instant_Translation": "Omset medan du skriv",
26 | "Mark_Unknown_Words": "Marker ukjende ord",
27 | "Multi_Step_Translation": "Fleirstegsomsetjing (eksperimentelt)",
28 | "Translate_Document": "Omset eit dokument",
29 | "Drop_Document": "Slepp eit dokument her",
30 | "Not_Available": "Omsetjing ikkje tilgjengeleg!",
31 | "File_Too_Large": "Fila er for stor!",
32 | "Cancel": "Avbryt",
33 | "Format_Not_Supported": "Det formatet er ikkje støtta!",
34 | "Translate_Webpage": "Omset ei nettside",
35 | "Supported_Formats": "Støtta format: .txt, .rtf, .odp, .ods, .odt (LibreOffice/OpenOffice), .xlsx, .pptx, .docx (Microsoft Office 2003 og nyare). Word 97 .doc-filer og PDF-ar er ikkje støtta. ",
36 | "Morphological_Analysis": "Morfologisk analyse",
37 | "Morphological_Analysis_Help": "Skriv inn tekst for morfologisk analyse.",
38 | "Analyze": "Analyser",
39 | "Morphological_Generation": "Morfologisk generering",
40 | "Morphological_Generation_Help": "Skriv inn morfologiske analysar som du vil generera ordformer av.",
41 | "Generate": "Generer",
42 | "Spell_Checker": "Stavekontroll",
43 | "APy_Sandbox": "APy-sandkasse",
44 | "APy_Sandbox_Help": "Send vilkårlege førespurnader",
45 | "APy_Request": "APy-førespurnad",
46 | "Request": "Førespurnad",
47 | "Language": "Språk",
48 | "Input_Text": "Inntekst",
49 | "Notice_Mistake": "Fann du ein feil?",
50 | "Help_Improve": "Hjelp oss å forbetra Apertium!",
51 | "Contact_Us": "Ta gjerne kontakt om du finn ein feil, har lyst til å jobba på eit prosjekt eller vil hjelpa oss på ein eller annan måte.",
52 | "Maintainer": "Denne nettstaden er halden vedlike av {{maintainer}}.",
53 | "About_Title": "Om nettstaden",
54 | "Enable_JS_Warning": "Denne nettstaden fungerer berre med JavaScript skrudd på. Viss du ikkje kan skru på JavaScript , kan du prøva omsetjarane til Prompsit .",
55 | "Not_Found_Error": "404-feil: Beklagar, den sida finst ikkje lenger!",
56 | "About": "Om",
57 | "Download": "Last ned",
58 | "Contact": "Ta kontakt",
59 | "Norm_Preferences": "Normval",
60 | "Documentation": "Dokumentasjon",
61 | "About_Apertium": "Om Apertium",
62 | "What_Is_Apertium": "Apertium er ei fri/open kjeldekode maskinomsetjingsplattform , opphavleg meint for nærskylde språk, men seinare utvida til å handtera meir ulike språkpar (som nordsamisk-bokmål). Plattformen tilbyr
ein språkuavhengig maksinomsetjingsmotor verktøy for å handtera dei språkdata som trengst for å byggja eit maskinomsetjingssystem for eit visst språkpar, og språkdata for fleire og fleire språkpar. Apertium vil gjerne ha nye utviklarar: Viss du trur du kan betra på motoren eller verktøya, eller utvikla språkdata for oss, må du ikkje nøla med å ta kontakt .
",
63 | "Documentation_Para": "Dokumentasjon finn du på wikien vår på sida Documentation . Me har publisert ei rekkje artiklar til konferansar og tidsskrift; du finn ei liste over desse på wikien på sida Publications .",
64 | "Apertium_Downloads": "Nedlastingar",
65 | "Downloads_Para": "Du finn siste utgåvene av Apertium-verktøya , i tillegg til språkdata , på GitHub-sida . Du finn installasjonsrettleiing for Apertium på alle vanlege plattformer på sida Installation på wikien.",
66 | "Contact_Para": "IRC-kanal Den raskaste måten å få kontakt med oss er ved å gå inn i IRC -kanalen vår, #apertium på irc.oftc.net, kor brukarar og utviklarar av Apertium møtest. Du treng ikkje å lasta ned eit IRC-program; det er nok å opna OFTC sin webchat .
E-postlister Du bør òg abonnera på e-postlista apertium-stuff , kor du kan leggja inn meir utbroderte forslag eller problem, og følgja med på ymse diskusjonar rundt Apertium.
Kontakt Ta gjerne kontakt med oss via e-postlista apertium-contact viss du finn ein feil eller har eit prosjekt du vil at me skal ta fatt i, eller har lyst til å hjelpa til.
",
67 | "Install_Apertium": "Installer Apertium",
68 | "Install_Apertium_Para": "Går det treigt? Maskinene våre er kanskje overleste. Lær korleis du kan installera Apertium på maskina di ."
69 | }
70 |
--------------------------------------------------------------------------------