diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8f21d94fb..4e580cdf3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,13 +1,12 @@ -🛑 **Only PRs related to localization are being accepted for Twine 2.4 right now.** \ -🛑 **Only PRs that fix bugs will be accepted for Twine 2.3. No PRs that add new features or modify existing functionality will be accepted.** - # Description Enter a short description of what this PR does. -# Issues fixed +# Issues Fixed + +_If you are contributing a localization, you can skip this section._ -Link any issues that this PR resolves. +Link the issues that this PR resolves. Your PR must resolve at least one issue, and agreement must be reached in the issue on your implementation approach before opening this PR. # Credit @@ -16,7 +15,7 @@ Please put an X in *one* of the squares below only. [ ] I would like to be credited in the application as: ______ \ [ ] I would not like my name to appear in the application credits. -# Presubmission checklist +# Presubmission Checklist Put an X in the squares below to indicate you've done each step. diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 000000000..991762675 --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,21 @@ +name: ESLint + +on: + push: + branches: [develop] + pull_request: + branches: [develop, main] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + cache: npm + node-version: 16 + - name: Install + run: npm ci + - name: Link + run: npm run lint diff --git a/.github/workflows/prs.yml b/.github/workflows/jest.yml similarity index 72% rename from .github/workflows/prs.yml rename to .github/workflows/jest.yml index 40e9ef469..f50ef5cd2 100644 --- a/.github/workflows/prs.yml +++ b/.github/workflows/jest.yml @@ -1,4 +1,4 @@ -name: PRs +name: Jest Tests on: push: @@ -7,17 +7,15 @@ on: branches: [develop, main] jobs: - lint-and-test: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: cache: npm - node-version: 14 + node-version: 16 - name: Install - run: npm install - - name: Lint - run: npm run lint + run: npm ci - name: Test run: npm run test:coverage -- --maxWorkers=2 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..660922e6e --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,28 @@ +name: Playwright Tests +on: + push: + branches: [develop] + pull_request: + branches: [develop, main] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + cache: npm + node-version: 16 + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test --reporter=line + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 70e5b31f1..60c884123 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ dist/* .idea electron-build/* coverage/* -docs/en/book/* \ No newline at end of file +docs/en/book/* +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d57766085..bd054f827 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,97 @@ -# Contributing code +# Contributing Code -Sorry, the 2.4 branch is still not quite ready for code contributions yet! Stay -tuned. +## Process -Bug fixes and updates for the built-in story formats are being accepted for the 2.3 branch. Please open a PR against the `2.3-maintenance` branch. +In general, the _last_ step in the process of contributing code is to open a PR +on this repository. The reason why is that writing code is time-consuming, and +it's better to get agreement on the implementation approach early instead of +having to make considerable revisions. -# Contributing localizations +## Bugfixes + +Please first open an issue on this repo and check the box in the issue form +indicating that you would like to work on a fix. We'll need to come to agreement +on how to fix the issue in discussion on that issue. For minor or obvious bugs, +this discussion should be very straightforward. + +Once agreement has been reached, a PR can be opened; please mention the issue +number in the PR's description. It will be assigned to a GitHub project for the +release it will be targeted to, so you can track the progress of a PR toward a +finished release. + +## New Features and Enhancements + +Please read [Twine's design goals] first. + +If you think your idea meshes with these goals, open an issue on this repo and +check the box in the issue form indicating you would like to work on a fix. We +will need to discuss your idea in detail and come to agreement on how it will +work. This process will likely require you to provide mock screenshots and +explain how users will use the feature in detail. + +Once we have come to agreement on the UI and implementation approach, a PR can +be opened. Please mention the issue number in the PR's description. As with +bugfixes, PRs are assigned to GitHub projects to track releases. + +## 2.3 + +PRs are no longer being accepted for the 2.3 release branch. Members of the +community who would like to continue to update 2.3 are welcome to do so in a +forked version of the app. (If you decide to do this, please use a different +name than Twine for the app.) + +## PR Practices + +PRs should always: + +- Target the `develop` branch, not `main`. +- Include sufficient unit test coverage. You can see a test coverage report by + running `npm run test:coverage`. A good guideline to deciding whether test + coverage is enough is to ask yourself, "Could this feature be re-implemented + solely by looking at the unit tests?" + - Unit tests for UI components should cover their behavior. Appearance does + not need to be unit tested. + - Unit tests for UI components should always include a baseline accessibility + test using [jest-axe]. + - Use test components like `` to test resulting state + instead of mocking store dispatches. + - Put tests in a `__tests__` directory, mocks in `__mocks__`. +- Include updates to the documentation if a feature is changing. +- Pass `npm run lint` checks with no warnings or errors. +- Use [prettier] for code formatting. There is a `prettier.config.js` file that + does a little configuration of the tool. + +## Code Practices + +- All data changes should take place through stores defined under `src/store`. + Components should manage as little internal state as possible. +- Conversely, code under `src/store` should never touch UI code directly. The + only way these two should interact is by a component dispatching actions in + the relevant store. +- Components under `src/components` should always take values and objects as + props, as opposed to IDs that they look up in a store. Code in `src/dialogs` + or `src/routes` may interact with stores. + - ✅ `` + - ❌ `` +- Components unders `src/components` should be [controlled] unless absolutely + necessary. This applies to all types of components, not just form fields. + - ✅ `` + - ❌ `` + - ✅ `` + - ❌ `` +- Every React component should be assigned a top-level CSS class that is the + React component name in kebab case. All related CSS rules should use this + class name for scoping. This ensures that components will not overwrite each + other's styles. +- Moving business logic out of React components and into `src/util` is almost + always a good idea. +- Use CSS variables defined in `src/styles` as much as possible. +- Use external libraries instead of reinventing the wheel if possible. +- Add external type definitions to `src/externals.d.ts` as a last resort. + Modules that come with type definitions, or that have DefinitelyTyped types, + are strongly preferred. + +# Contributing Localizations Twine's localization strings are stored in [i18next] JSON format. There are a number of dedicated editors for this format, or you can just use a plain text @@ -16,14 +102,22 @@ To add a new localization or edit an existing one: 1. Clone the application source code using Git. 2. Create a new branch for your work. 3. Create or edit the appropriate file in `public/locales`. The file should be - named after the language code you are localizing for. (Check [the registry](lang-code-registry) to find the appropriate code). + named after the language code you are localizing for. (Check [the + registry](lang-code-registry) to find the appropriate code). 4. If you are creating a new localization, copy the existing `en-US.json` file and replace the English strings there with localized ones in the new file. 5. Commit your changes and create a pull request in GitHub. You should target the `develop` branch with your pull request. Once your PR has been accepted, please join the Twine internationalization -listserv by sending an email to `twine-i18n-join@iftechfoundation.org`. This is a low-traffic listserv that will be used to notify people who have worked on localization on Twine when future versions require localization work, e.g. when new text is added to the application. +listserv by sending an email to `twine-i18n-join@iftechfoundation.org`. This is +a low-traffic listserv that will be used to notify people who have worked on +localization on Twine when future versions require localization work, e.g. when +new text is added to the application. +[jest-axe]: https://www.npmjs.com/package/jest-axe [i18next]: https://www.i18next.com/ -[lang-code-registry]: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry \ No newline at end of file +[lang-code-registry]: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry +[prettier]: https://prettier.io +[controlled]: https://reactjs.org/docs/forms.html#controlled-components +[Twine's design goals]: DESIGN_GOALS.md \ No newline at end of file diff --git a/DESIGN_GOALS.md b/DESIGN_GOALS.md new file mode 100644 index 000000000..1be08e0f6 --- /dev/null +++ b/DESIGN_GOALS.md @@ -0,0 +1,119 @@ +# Design Goals + +This documents the design goals (and non-goals) Twine has. They're intended to +guide discussion around feature suggestions and future development. Twine, like +any piece of software, isn't perfect, and so it may not entirely live up to the +goals stated here. + +Each of the goals has a set of bullet points that discuss their implications in +practice, but they shouldn't be considered a complete list. + +## Easy to Learn + +It takes around 5-10 minutes to explain how to use Twine to make a story with +basic links. This simplicity is key to Twine's success. A core part of Twine's +audience are people who have had no previous programming experience and may not +even be particularly knowledgable about computers. + +- Twine avoids features that are complicated to explain to a new user. These + features are often better-suited to tools aimed at more advanced users, like + [extwee] and [tweego]. Twine doesn't exclude use by advanced users, but it + prioritizes beginners. +- Twine prefers providing users with sensible defaults that can be changed later + instead of blocking actions and asking for decisions. For example, creating a + new passage creates it with a placeholder name instead of showing a modal + dialog asking the user to provide a name, when the user may not know what they + want to call it yet. +- Twine avoids using [modes], allowing users to work on multiple tasks in + parallel, or to start a task and come back to it later. +- Actions in Twine are undoable, allowing users to easily reverse mistakes. +- Twine's user interface explains itself to users, and strives to be + discoverable. + - It re-uses interface patterns users are likely to already be familiar with + when possible. + - It includes explanatory links like "What is a story format?" + - It avoids technical jargon. + - Features are not placed in contextual menus or available through keyboard + shortcuts only, where only a user who has read documentation might know about + them. + - It uses icon-only buttons extremely sparingly. The intent of a button + labeled with text is almost always clearer than one that only has an icon. +- [The Twine Reference][twine-ref] comprehensively covers Twine's features. + (It's not a tutorial or how-to guide to writing with Twine, though--other + community resources serve that purpose.) + +## Accessible + +There are several dimensions in which Twine strives to be accessible. The +dimensions listed here are equally important. + +Twine is accessible to users with disabilities. + +- User interface development is guided by [WCAG guidelines]. In particular: + - Twine is usable for users who use screen readers like JAWS, NVDA, or + VoiceOver. + - As much of Twine as possible is usable by a someone who only uses a + keyboard. Many users only use a keyboard or assistive technology that + emulates a keyboard. +- Twine is covered by unit tests that check for basic accessibility problems. + These unit tests are not comprehensive but serve as a baseline. + +Twine is accessible to users regardless of the language they speak. + +- No language or locale receives preferential treatment by Twine. + - Twine tries to detect the language the user's computer is set to, instead of + defaulting to US English on first startup. +- All language in Twine is localized. +- Twine properly handles input for users who use right-to-left languages. + +Twine is accessible to users who interact with their computer with touchscreen +input, as well as those who use mouse and keyboard. + +- Interaction targets like buttons and text fields are sized and spaced so that + they can be used comfortably on a touchscreen. +- Interactions triggered by pointing a cursor at something in Twine are avoided, + because these don't have an equivalent on most touchscreens. If they do exist, + alternatives that are usable on touchscreens also exist. + +## Web Native + +Twine's home is the web. Although many dedicated Twine users use it in its +desktop app version (abbreivated here as "app Twine"), there is a significant +population of users who use the online version (abbreviated "browser Twine"). + +It's difficult to estimate an exact number of browser Twine users because, out +of respect for users' privacy, Twine does not include tracking like Google +Analytics. But in one month in 2022, there were more than 100,000 requests for +browser Twine at https://twinery.org/2 in server logs. (This number excludes +known crawlers like Google, as well as requests for all of the assets that the +online editor loads.) A request is more-or-less a single editing session in +browser Twine, so most likely a single person using Twine in a month will +generate multiple requests. Regardless, the point here is that a significant +number of users regularly use browser Twine. + +Staying web native has also meant that adapting Twine for multiple platforms has +been relatively easy thanks to projects like [Electron], and it ensures that +Twine will be usable for years if not decades to come. + +- App Twine and browser Twine users have, as much as possible, identical + experiences. +- App Twine users have identical experiences regardless of what operating system + they use. +- Browser Twine supports as many modern browsers as is feasible, and users of + browser Twine should have identical experiences regardless of which browser + they use. + - The major exception is Safari, which has imposed [restrictions on local + storage](safari-localstorage) which are admirable in their goal of + protecting user privacy, but have dire implications for Twine users, who can + easily lose all of their work if they aren't careful. If it becomes possible + to use browser Twine in Safari safely, it would be worthwhile to make this happen. +- The load time--which loosely equates to the download size--of Twine matters + and new dependencies should be carefully considered before being adopted. + +[extwee]: https://github.com/videlais/extwee +[tweego]: https://www.motoslave.net/tweego/ +[modes]: https://en.wikipedia.org/wiki/Mode_(user_interface) +[wcag guidelines]: https://www.w3.org/WAI/standards-guidelines/wcag/ +[twine-ref]: https://twinery.org/reference/en/ +[electron]: https://www.electronjs.org +[safari-localstorage]: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ diff --git a/README.md b/README.md index 2ac60b34d..f7fcbef2b 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ This is a port of Twine to a browser and Electron app. See The story formats in minified format under `story-formats/` exist in separate repositories: -- [Harlowe](https://bitbucket.org/_L_/harlowe) +- [Harlowe](https://foss.heptapod.net/games/harlowe/) - [Paperthin](https://github.com/klembot/paperthin) - [Snowman](https://github.com/klembot/snowman) -- [SugarCube](https://bitbucket.org/tmedwards/sugarcube) +- [SugarCube](https://github.com/tmedwards/sugarcube-2) ### INSTALL diff --git a/docs/en/src/editing-stories/editing-passages.md b/docs/en/src/editing-stories/editing-passages.md index 7f11c081b..92611582b 100644 --- a/docs/en/src/editing-stories/editing-passages.md +++ b/docs/en/src/editing-stories/editing-passages.md @@ -10,9 +10,9 @@ text you enter will be rendered by the story format when your story is played. For instance, you might enter code into your passage to set variables or conditionally display some text. -The font and size of the text can be customized in [Twine's preferences](../preferences). -This doesn't change what the passage looks like when played; it just lets you -make the text editor more comfortable to use. +The font and size of the text can be customized in [Twine's +preferences](../preferences). This doesn't change what the passage looks like +when played; it just lets you make the text editor more comfortable to use. Story formats can extend Twine to add syntax formatting to the passage text editor. For example, links might appear in a blue color. You'll need to consult @@ -23,13 +23,29 @@ extensions](../story-formats/extensions.md). Twine automatically saves your changes to a passage after you stop typing for a moment. +## Editing Multiple Passages + +If you edit a passage while another is open for editing, the new edit dialog +will appear on top of the existing one. Twine will keep up to five passage edit +dialogs open below the most recent one. If you open more than that, Twine will +close the oldest passage edit dialog for you. + +Click or tap a passage edit dialog in the background to bring it to the front, +or select the close button in the dialog to close it. + +If you have more than three passage edit dialogs open, Twine will overlap the +oldest to save screen space. Point or tap on the overlapped dialogs to reveal +them, and move your mouse away or tap elsewhere to restore them to their +previous state. + ## Automatically-Created Links As you enter text in a passage, Twine will detect when you've added new links. If the destination passage doesn't already exist, it will create an empty passage for you. Deleting the link will delete this empty passage. -Twine won't delete an empty passage while editing if any of the criteria below are true: +Twine won't delete an empty passage while editing if any of the criteria below +are true: - It is linked to from another passage - It has any tags diff --git a/docs/en/src/editing-stories/navigating.md b/docs/en/src/editing-stories/navigating.md index d001e52a7..07f053c3e 100644 --- a/docs/en/src/editing-stories/navigating.md +++ b/docs/en/src/editing-stories/navigating.md @@ -14,6 +14,25 @@ In one corner of the Story Map, you'll see three buttons showing squares of different sizes. These let you zoom in and out of the map, showing different levels of detail in your passages. +## Jumping to a Passage by Name or Text + +To move to a particular passage in the Story Map screen, choose _Go To_ from the +_Passage_ top toolbar tab, or press the `P` key any time your cursor is not in a +text field. + +This will open a dialog with a search field. Enter either the name of a passage +or some text it contains, and a list of matching passages will appear. Twine +uses fuzzy matching, so you don't have to enter the passage name exactly, and it +will find close matches if you make a typo. When deciding which pasages match +what you've typed, it slightly prefers matches in a passage name to what's in +passage text. + +Click a passage in the list or press the Return key to select the one which has +two chevrons (») beside it to move your view so that the passage you've chosen +is centered. The chosen passage will also be selected. Use the up and down arrow +keys on your keyboard to move the chevrons in the list to another passage in the +list of matches. + ## Empty Passages An empty passage is one you haven't written any text in (usually). These show up diff --git a/docs/en/src/getting-started/basic-concepts.md b/docs/en/src/getting-started/basic-concepts.md index e9bb6aada..47dc5f3a0 100644 --- a/docs/en/src/getting-started/basic-concepts.md +++ b/docs/en/src/getting-started/basic-concepts.md @@ -109,6 +109,16 @@ proofread your stories before you share it with a larger audience. Twine comes with one proofing format, called Paperthin, but others with more features exist in the Twine community. +## Twee + +Twee is a plain-text format for Twine stories. Twee files are not playable by +themselves, but are easier to edit and view in a text editor than the HTML files +that Twine creates. The Twee format is [documented +here](https://github.com/iftechfoundation/twine-specs/blob/master/twee-3-specification.md). + +You don't need to use Twee to build a story with Twine, but Twine can export +stories to Twee format for use with other tools. + [^start]: In Twine version 1, this passage had to be called "Start" (including the capital S). Current versions of Twine allow setting the start passage to any one in a story, regardless of name. \ No newline at end of file diff --git a/docs/en/src/story-library/creating.md b/docs/en/src/story-library/creating.md index 376c9289b..28fdf633e 100644 --- a/docs/en/src/story-library/creating.md +++ b/docs/en/src/story-library/creating.md @@ -20,21 +20,37 @@ _Story_ top toolbar tab. Twine will create a copy for you with a unique name. ## Importing Stories -Twine can import stories in progress, published stories, and exported archives. -It cannot, however, import stories from Twine 1 or Twee source code. +Twine can import stories in progress, published stories, exported archives, and +Twee source code. It cannot, however, import stories from Twine 1. To import stories or archives, the process is the same: 1. Choose _Import_ from the _Library_ top toolbar tab. -2. In the dialog that appears, choose the HTML file corresponding to your story +2. In the dialog that appears, choose the file corresponding to your story or archive. If the file you want to import is disabled in the file dialog, it's because it's in a format that can't be used by Twine. -3. The dialog will show the story or stories Twine found in your file. Select - the ones you want to import. The dialog'll warn you if a story you're - importing has the same name as one already in your library. **If you do - choose to import it, it will overwrite your existing story completely.** -4. Use the _Import Selected Files_ button in the dialog to import the files +3. If the stories in the file you selected don't have the same name as any story + already in your library, Twine will import them immediately. +4. Otherwise, the dialog will show the story or stories Twine found in your + file. Select the ones you want to import; stories that won't overwrite + existing stories in your library are checked off for you by default. The + dialog'll warn you if a story you're importing has the same name as one + already in your library. **If you do choose to import it, it will overwrite + your existing story completely.** +5. Use the _Import Selected Files_ button in the dialog to import the files you've selected. If you change your mind about importing midway through the process, close the -dialog or choose a different file to restart the process. \ No newline at end of file +dialog or choose a different file to restart the process. + +## Twee Import Limitations + +Twine will use the story and passage metadata present in Twee source code, such +as passage position or story name. If this metadata is not present, Twine will +try to substitute reasonable defaults, but it will not handle all cases +perfectly. In particular: + +- If Twee source code does not include passage positions, Twine will place + passages in a grid pattern. +- If a Twee file does not specify what story format and version it uses, Twine + will set it to [the default story format](../story-formats/default.html). diff --git a/docs/en/src/story-library/exporting.md b/docs/en/src/story-library/exporting.md index 25f336930..77a49d7fd 100644 --- a/docs/en/src/story-library/exporting.md +++ b/docs/en/src/story-library/exporting.md @@ -19,4 +19,15 @@ This file can be either opened directly in a web browser to play your story, or [imported into Twine](creating.md). The other buttons under the _Build_ tab work the same as they do in the [Story -Map Screen](../editing-stories). \ No newline at end of file +Map Screen](../editing-stories). + +## Exporting a Story To Twee + +You can also export a story to [Twee +format](../getting-started/basic-concepts.html#twee). Select it and choose +_Export as Twee_ from the _Build_ top toolbar tab. You'll be asked where to save +this file. + +Twine creates Twee files with a `.twee` file suffix. Your system may not know +how to handle them by default, but they are openable in any plain text editor, +like Notepad on Windows or TextEdit on macOS. \ No newline at end of file diff --git a/e2e/smoke-test.spec.ts b/e2e/smoke-test.spec.ts new file mode 100644 index 000000000..4c06f2c9e --- /dev/null +++ b/e2e/smoke-test.spec.ts @@ -0,0 +1,152 @@ +import {test, expect, Page} from '@playwright/test'; + +async function skipWelcome(page: Page) { + await page.goto('http://localhost:3000'); + await page.getByRole('button', {name: 'Skip'}).click(); + await page.reload(); +} + +async function createStory(page: Page, name = 'E2E Test Story') { + await skipWelcome(page); + await page.getByRole('tab', {name: 'Story'}).click(); + await page.getByRole('button', {name: 'New'}).click(); + await page + .getByRole('textbox', { + name: 'What should your story be named? You can change this later.' + }) + .type(name); + await page.getByRole('button', {name: 'Create'}).click(); +} + +async function openPassageEditor(page: Page, name: string) { + await page.getByRole('button', {name}).click(); + await expect(page.getByRole('button', {name})).toHaveAttribute( + 'aria-pressed', + 'true' + ); + await page.getByRole('tab', {name: 'Passage'}).click(); + await page.getByRole('button', {name: 'Edit'}).click(); +} + +async function waitForPassageChange() { + // Although the DOM updates when a passage is edited, actually saving changes + // is debounced. This waits long enough for a change to complete. + await new Promise(resolve => setTimeout(resolve, 1100)); +} + +test('Shows welcome screen on first run', async ({page}) => { + await page.goto('http://localhost:3000'); + await expect(page).toHaveTitle('Hi!'); +}); + +test("Doesn't show welcome screen after user finishes it", async ({page}) => { + await skipWelcome(page); + await expect(page).toHaveTitle('0 Stories'); + await page.goto('http://localhost:3000'); + await expect(page).toHaveTitle('0 Stories'); + await page.reload(); + await expect(page).toHaveTitle('0 Stories'); +}); + +test('Can create a story', async ({page}) => { + await createStory(page, 'Create story test'); + await expect(page).toHaveTitle('Create story test'); + + // If these tabs are visible, we're in the story editor. + + await expect(page.getByRole('tab', {name: 'Passage'})).toBeVisible(); + await expect(page.getByRole('tab', {name: 'Story'})).toBeVisible(); + + // Go back to the story list and make sure the story is present there. + + await page.goto('http://localhost:3000'); + await expect(page).toHaveTitle('1 Story'); + await expect(page.getByText('Create story test')).toBeVisible(); + await page.reload(); + await expect(page).toHaveTitle('1 Story'); + await expect(page.getByText('Create story test')).toBeVisible(); +}); + +test('Persists passage edits', async ({page}) => { + await createStory(page, 'Edit passage test'); + await openPassageEditor(page, 'Untitled Passage'); + + // Test different typing speeds to try to shake out any problems with the + // debounced update. + + await page.getByLabel('Passage Text').type('abcdef', {delay: 0}); + await page.getByLabel('Passage Text').type('ghijkl', {delay: 100}); + await page.getByLabel('Passage Text').type('mnopqr', {delay: 250}); + await page.getByLabel('Passage Text').type('stuvwx', {delay: 500}); + await expect(page.getByText('abcdefghijklmnopqrstuvwx')).toBeVisible(); + await waitForPassageChange(); + await page.reload(); + await expect(page.getByText('abcdefghijklmnopqrstuvwx')).toBeVisible(); +}); + +test('Persists passage renames', async ({page}) => { + await createStory(page, 'Edit passage test'); + await page.getByRole('button', {name: 'Untitled Passage'}).click(); + await page.getByRole('button', {name: 'Rename'}).click(); + await page + .getByRole('textbox', { + name: 'What should “Untitled Passage” be renamed to?' + }) + .type('Rename test'); + await page.getByRole('button', {name: 'OK'}).click(); + await expect(page.getByRole('button', {name: 'Rename test'})).toBeVisible(); + await page.reload(); + await expect(page.getByRole('button', {name: 'Rename test'})).toBeVisible(); +}); + +test('Creates a simple story and plays it', async ({context, page}) => { + await createStory(page, 'Publish test'); + await openPassageEditor(page, 'Untitled Passage'); + await page + .getByLabel('Passage Text') + .type('Which way to go? [[Left]] or [[right]]?'); + await page.getByRole('button', {name: 'Close'}).click(); + await openPassageEditor(page, 'Left'); + await page.getByLabel('Passage Text').type('Monsters!'); + await waitForPassageChange(); + await page.getByRole('button', {name: 'Close'}).click(); + + // Wait for the editor to close. + + await expect(page.getByLabel('Passage Text')).not.toBeVisible(); + await openPassageEditor(page, 'right'); + await page.getByLabel('Passage Text').type('Puppies!'); + await waitForPassageChange(); + await page.getByRole('button', {name: 'Close'}).click(); + await page.getByRole('tab', {name: 'Build'}).click(); + + const [publishedPage] = await Promise.all([ + context.waitForEvent('page'), + page.getByRole('button', {name: 'Play'}).click() + ]); + + // Trying to be as agnostic as possible about Harlowe's DOM structure. Visible + // locators are to distinguish from passage data that's in the DOM but not + // visible. + + await publishedPage.waitForSelector(':visible:text-is("Which way to go?")'); + await publishedPage.locator(':visible:text-is("Left")').click(); + await publishedPage.waitForSelector(':visible:text-is("Monsters!")'); + await expect( + publishedPage.locator(':visible:text-is("Monsters!")') + ).toBeVisible(); + + // Need to close the tab to reset play state. Reloading won't work. + + await publishedPage.close(); + + const [republishedPage] = await Promise.all([ + context.waitForEvent('page'), + page.getByRole('button', {name: 'Play'}).click() + ]); + + await republishedPage.locator(':visible:text-is("right")').click(); + await expect( + republishedPage.locator(':visible:text-is("Puppies!")') + ).toBeVisible(); +}); diff --git a/electron-builder.config.js b/electron-builder.config.js index bd86c050c..e229e981f 100644 --- a/electron-builder.config.js +++ b/electron-builder.config.js @@ -5,21 +5,21 @@ const isPreview = /alpha|beta|pre/.test(pkg.version) || process.env.FORCE_PREVIEW; module.exports = { - afterSign(context) { - // This step is necessary to ad hoc sign the app. Otherwise, on Apple - // Silicon you get repeated prompts for file access. - // - // If/when we are able to sign the app for real, this must be removed. - // - // This was cribbed from https://github.com/alacritty/alacritty/issues/5840. - - if (context.packager.platform.name === 'mac') { - console.log('Ad hoc signing Mac app...'); - child_process.execSync( - 'codesign --force --deep --sign - dist/electron/mac-universal/Twine.app' - ); - } - }, + // This step was necessary to ad hoc sign the app. Otherwise, on Apple Silicon + // you get repeated prompts for file access. This is commented out because we + // are able to sign the app thanks to the Interactive Fiction Technology + // Foundation, but originally figuring this problem out took forever, so the + // code below might be helpful to others making builds. + // The code below was cribbed from https://github.com/alacritty/alacritty/issues/5840. + // + // afterSign(context) { + // if (context.packager.platform.name === 'mac') { + // console.log('Ad hoc signing Mac app...'); + // child_process.execSync( + // 'codesign --force --deep --sign - dist/electron/mac-universal/Twine.app' + // ); + // } + // }, directories: { output: 'dist/electron' }, diff --git a/package-lock.json b/package-lock.json index 0d9e8819a..44e05657d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Twine", - "version": "2.4.1", + "version": "2.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "Twine", - "version": "2.4.1", + "version": "2.5.1", "license": "GPL-3.0", "dependencies": { "@popperjs/core": "^2.9.1", @@ -20,6 +20,7 @@ "focus-trap-react": "^8.9.2", "focus-visible": "^5.2.0", "fs-extra": "^10.0.0", + "fuse.js": "^6.6.2", "i18next": "^19.9.2", "i18next-http-backend": "^1.1.1", "is-absolute-url": "^3.0.3", @@ -48,6 +49,7 @@ "use-error-boundary": "^2.0.6" }, "devDependencies": { + "@playwright/test": "^1.27.1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/react-hooks": "^5.1.1", @@ -2906,6 +2908,22 @@ "node": ">=10" } }, + "node_modules/@playwright/test": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz", + "integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.27.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz", @@ -13392,6 +13410,14 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "node_modules/fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -20580,6 +20606,18 @@ "node": ">=4" } }, + "node_modules/playwright-core": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz", + "integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/plist": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", @@ -31545,6 +31583,16 @@ } } }, + "@playwright/test": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz", + "integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.27.1" + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz", @@ -40176,6 +40224,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -45811,6 +45864,12 @@ } } }, + "playwright-core": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz", + "integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==", + "dev": true + }, "plist": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", diff --git a/package.json b/package.json index 195b33d61..37700ca39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Twine", - "version": "2.5.1", + "version": "2.6.0", "description": "a GUI for creating nonlinear stories", "author": "Chris Klimas ", "license": "GPL-3.0", @@ -25,6 +25,7 @@ "focus-trap-react": "^8.9.2", "focus-visible": "^5.2.0", "fs-extra": "^10.0.0", + "fuse.js": "^6.6.2", "i18next": "^19.9.2", "i18next-http-backend": "^1.1.1", "is-absolute-url": "^3.0.3", @@ -53,6 +54,7 @@ "use-error-boundary": "^2.0.6" }, "devDependencies": { + "@playwright/test": "^1.27.1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/react-hooks": "^5.1.1", @@ -105,6 +107,7 @@ "build:electron-main": "tsc --project tsconfig.electron.json", "build:electron-bundle": "electron-builder --config electron-builder.config.js --mac --windows --linux --publish=never", "clean": "rimraf dist electron-build", + "e2e": "playwright test", "lint": "eslint src", "start": "react-scripts start", "start:docs": "cd docs/en && mdbook serve", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..002138b87 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,107 @@ +import type {PlaywrightTestConfig} from '@playwright/test'; +import {devices} from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './e2e', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry' + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'] + } + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'] + } + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'] + } + } + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run start', + port: 3000 + } +}; + +export default config; diff --git a/public/locales/de.json b/public/locales/de.json index cb2394971..3489dc514 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -26,6 +26,7 @@ "editCount": "Bearbeitung ({{count}})", "help": "Hilfe", "import": "Import", + "maximize": "Maximieren", "more": "Mehr", "new": "Neu", "next": "Weiter", @@ -48,6 +49,7 @@ "twine": "Twine", "undo": "Rückgängig", "undoChange": "Rückgang {{change}}", + "unmaximize": "Größe zurücksetzen", "view": "Ansicht" }, "components": { @@ -86,9 +88,13 @@ "percentAvailable": "{percent}% Speicherplatz frei" }, "passageCard": { - "placeholderClick": "Doppelklicke auf diesen Abschnitt um ihn zu bearbeiten.", + "placeholderClick": "Doppelklick auf diesen Abschnitt um ihn zu bearbeiten.", "placeholderTouch": "Tippe auf den Abschnitt, dann gehe auf Bearbeiten im Abschnittstab um ihn zu bearbeiten." }, + "passageFuzzyFinder": { + "noResults": "Keine Treffer.", + "prompt": "Suche nach Abschnitts Name oder Text" + }, "renamePassageButton": { "emptyName": "Bitte gib einen Namen ein.", "nameAlreadyUsed": "Ein anderer Abschnitt dieser Geschichte hat diesen Namen." @@ -139,7 +145,7 @@ "aboutTwine": { "donateToTwine": "Unterstütze Twine mit einer Spende", "codeHeader": "Code", - "codeRepo": "Besuche Source Code Repository", + "codeRepo": "Besuche das Source Code Repository", "license": "Diese Anwendung ist unter der GPL v3 Lizenz veröffentlicht, aber ein Werk, das mit dieser Anwendung erstellt wurde, darf unter einer beliebigen auch kommerziellen Lizenz veröffentlicht werden.", "localizationHeader": "Lokalisierungen", "title": "Über Twine {{version}}", @@ -155,8 +161,14 @@ "appPrefs": { "codeEditorFont": "Code Editor Font", "codeEditorFontScale": "Code Editor Font Größe", + "dialogWidth": "Dialog Breite", + "dialogWidths": { + "default": "Default", + "wider": "Breiter", + "widest": "Am Breitesten" + }, "editorCursorBlinks": "Blinkender Cursor in den Editoren", - "fontExplanation": "Den Font hier zu ändern betrifft nur den Twine Editor. Es wird nicht den Font den eine Geschichte beim spielen benutzt ändern.", + "fontExplanation": "Font Änderungen an dieser Stelle betreffen nur den Twine Editor. Es wird nicht der Font den eine Geschichte beim Spielen benutzt ändern.", "language": "Sprache", "passageEditorFont": "Abschnitts Editor Font", "passageEditorFontScale": "Abschnitts Editor Font Größe", @@ -183,7 +195,8 @@ }, "storyImport": { "deselectAll": "Alle Markierungen aufheben", - "filePrompt": "Um Geschichten nach Twine zu importieren, lade unten entweder ein Archiv oder eine Datei einer veröffentlichen Geschichte hoch.", + "filePrompt": "Um Geschichten nach Twine zu importieren, lade unten entweder ein Archiv, eine Datei einer veröffentlichen Geschichte oder eine Twee Quelldatei hoch.", + "filePrompt": "To import stories into Twine, upload an archive, published story, or Twee source file below.", "importDifferentFile": "Importiere eine andere Datei", "importSelected": "Importiere die ausgewählten Dateien", "importThisStory": "Importiere Diese Geschichte", @@ -270,6 +283,7 @@ "storyEdit": { "toolbar": { "findAndReplace": "Suchen und Ersetzen", + "goTo": "Gehe Zu", "javaScript": "JavaScript", "passageTags": "Abschnitt Tags", "snapToGrid": "Am Raster ausrichten", @@ -369,6 +383,7 @@ "storyFormats": "Geschichtsformate" }, "build": { + "exportAsTwee": "Exportiere als Twee", "play": "Spiele", "proof": "Korrektur", "publishToFile": "Als Datei veröffentlichen", diff --git a/public/locales/en-US.json b/public/locales/en-US.json index f0c64b0ec..35e7b31cf 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -91,6 +91,10 @@ "placeholderClick": "Double-click this passage to edit it.", "placeholderTouch": "Tap this passage, then choose Edit from the Passage tab to edit it." }, + "passageFuzzyFinder": { + "noResults": "No matches.", + "prompt": "Search by passage name or text" + }, "renamePassageButton": { "emptyName": "Please enter a name.", "nameAlreadyUsed": "Another passage in this story has this name." @@ -191,7 +195,7 @@ }, "storyImport": { "deselectAll": "Deselect All", - "filePrompt": "To import stories into Twine, upload either an archive or published story file below.", + "filePrompt": "To import stories into Twine, upload an archive, published story, or Twee source file below.", "importDifferentFile": "Import a Different File", "importSelected": "Import Selected Files", "importThisStory": "Import This Story", @@ -278,6 +282,7 @@ "storyEdit": { "toolbar": { "findAndReplace": "Find and Replace", + "goTo": "Go To", "javaScript": "JavaScript", "passageTags": "Passage Tags", "snapToGrid": "Snap to Grid", @@ -377,6 +382,7 @@ "storyFormats": "Story Formats" }, "build": { + "exportAsTwee": "Export As Twee", "play": "Play", "proof": "Proof", "publishToFile": "Publish to File", diff --git a/public/locales/tr.json b/public/locales/tr.json index e75d6b62b..8ec4e8c65 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -1,88 +1,362 @@ { - "colors": {}, + "colors": { + "none": "Renksiz", + "red": "Kırmızı", + "orange": "Turuncu", + "yellow": "Sarı", + "green": "Yeşil", + "blue": "Mavi", + "purple": "Mor" + }, "common": { "add": "Ekle", "appName": "Twine", + "back": "Geri", + "build": "İnşa Et", "cancel": "İptal", + "close": "Kapat", + "color": "Renk", + "create": "Oluştur", + "custom": "Özel", "delete": "Sil", + "deleteCount": "Sil ({{count}})", + "details": "Detaylar", "duplicate": "Kopya Oluştur", - "edit": "Düzen", + "edit": "Düzenle", + "editCount": "Düzenle ({{count}})", + "help": "Yardım", + "import": "İçe Aktar", + "maximize": "Kapla", + "more": "Daha Fazla", + "new": "Yeni", + "next": "Sonraki", "ok": "Tamam", - "play": "Oku", + "passage": "Bölüm", + "play": "Oynat", + "preferences": "Tercihler", + "publishToFile": "Dosyaya Yayımla", + "redo": "İleri Al", + "redoChange": "İleri Al ({{change}})", "rename": "Yeniden Adlandır", + "renamePrompt": "“{{name}}“ nasıl yeniden adlandırılsın?", "remove": "Kaldır", + "selectAll": "Hepsini Seç", "skip": "Atla", + "story": "Öykü", "storyFormat": "Öykü Biçimi", "tag": "Etiket", "test": "Dene", - "undo": "Geri Al" + "twine": "Twine", + "undo": "Geri Al", + "undoChange": "Geri Al ({{change}})", + "unmaximize": "Önceki Boyut", + "view": "Görünüm" }, "components": { "addStoryFormatButton": { "prompt": "Öykü biçimi eklemek için biçimin internet adresini aşağı girin." }, - "addTagButton": {}, - "fontSelect": {"fonts": {}}, - "indentButtons": {}, - "localStorageQuota": {}, + "addTagButton": { + "alreadyAdded": "Bu etiket adı zaten daha önceden eklendi.", + "addLabel": "Etiket Ekle", + "invalidName": "Lütfen geçerli bir etiket adı girin.", + "newTag": "Yeni Etiket", + "tagColorLabel": "Etiket Rengi", + "tagNameLabel": "Etiket Adı" + }, + "dialogCard": { + "contentsCrashed": "Bu iletişim kutusunda bir şeyler ters gitti. Lütfen kapatıp tekrar açmayı deneyin." + }, + "fontSelect": { + "customScaleDetail": "Lütfen yalnızca yüzdelik değer girin.", + "customFamilyDetail": "Lütfen yalnızca yazı tipinin adını girin.", + "familyEmpty": "Lütfen bir yazı tipi adı girin.", + "font": "Yazı Tipi", + "fonts": { + "monospaced": "Eşit Aralık (Monospace)", + "serif": "Serifli", + "system": "Sistem" + }, + "fontSize": "Yazı Boyutu", + "percentage": "%{{percent}}", + "percentageIsntNumber": "Lütfen bir sayı giriniz.", + "percentageNotPositive": "Lütfen sıfırdan büyük bir sayı giriniz." + }, + "indentButtons": { + "indent": "Girintile", + "unindent": "Girintiyi Çıkar" + }, + "localStorageQuota": { + "measureAgain": "Mevcut alanı tekrar ölçün", + "percentAvailable": "%{{percent}} yer mevcut" + }, "passageCard": { "placeholderClick": "Bölümü düzenlemek için çift tıklayın.", - "placeholderTouch": "Önce bu bölüme, sonra kaleme basarak bölümü düzenleyebilirsiniz." - }, - "renamePassageButton": {"emptyName": "Lütfen bir isim girin."}, - "renameStoryButton": {"emptyName": "Lütfen bir isim girin."}, - "safariWarningCard": {}, - "storyCard": {}, - "storyFormatCard": {}, - "storyFormatSelect": {}, - "tagEditor": {} + "placeholderTouch": "Önce bu bölümü seçerek, ardından Bölüm sekmesinden Düzenleye basarak bölümü düzenleyebilirsiniz." + }, + "renamePassageButton": { + "emptyName": "Lütfen bir isim girin.", + "nameAlreadyUsed": "Bu ada başka bir bölüm sahip." + }, + "renameStoryButton": { + "emptyName": "Lütfen bir isim girin.", + "nameAlreadyUsed": "Bu ada başka bir öykü sahip." + }, + "safariWarningCard": { + "archiveAndUseAnotherBrowser": "Lütfen öykülerinizi arşivleyin ve başka bir platform kullanın.", + "addToHomeScreen": "Bu sınırlamayı aşmak için bu siteyi ana ekranınıza ekleyin.", + "howToAddToHomeScreen": "Ana Ekranıma Nasıl Eklerim?", + "learnMore": "Daha Fazla Öğren", + "message": "Kullandığınız tarayıcı siteyi yedi gün boyunca kullanmadığınız takdirde tüm öykülerinizi siler." + }, + "storageQuota": { + "freeSpace": "%{{percent}} alan mevcut" + }, + "storyCard": { + "lastUpdated": "En son {{date}} tarihinde düzenlendi", + "passageCount": "1 Bölüm", + "passageCount_plural": "{{count}} Bölüm" + }, + "storyFormatCard": { + "author": "{{author}} tarafından", + "builtIn": "Dahili", + "defaultFormat": "Varsayılan biçim.", + "editorExtensionsDisabled": "Düzenleyici Eklentileri Kapalı", + "license": "Lisans: {{license}}", + "loadingFormat": "Bu öykü biçimi yükleniyor...", + "loadError": "Bu öykü biçimi yüklenemedi. ({{errorMessage}})", + "name": "{{name}} {{version}}", + "proofing": "İmla Düzeltme", + "proofingFormat": "İmla hatalarını düzeltmek için kullanılır.", + "useEditorExtensions": "Düzenleyici Eklentilerini Kullan", + "useFormat": "Varsayılan Öykü Biçimi Olarak Kullan", + "useProofingFormat": "İmla Düzeltme Biçimi Olarak Kullan" + }, + "storyFormatSelect": { + "loadingCount": "1 Öykü Biçimi Yükleniyor...", + "loadingCount_plural": "{{loadingCount}} Öykü Biçimi Yükleniyor" + }, + "tagEditor": { + "alreadyExists": "Bu isme sahip bir etiket zaten var." + } }, "dialogs": { "aboutTwine": { "donateToTwine": "Twine'ın Büyümesine Bağış Yaparak Katkıda Bulunun", - "codeHeader": "Kod Yazarları" + "codeHeader": "Kod Yazarları", + "codeRepo": "Kaynak Deposunu Ziyaret Et", + "license": "Bu yazılım GPL v3 Lisansı koşulları altında yayımlanmaktadır ancak bu yazılım kullanılarak oluşturulan herhangi bir eser ticari amaçlar da dahil olmak üzere herhangi bir koşulda yayımlanabilir.", + "localizationHeader": "Çeviriler", + "title": "Twine Hakkında {{version}}", + "twineDescription": "Twine, interaktif ve doğrusal olmayan öyküler oluşturmak için kullanılan bir açık kaynaklı yazılımdır." + }, + "appDonation": { + "donate": "Twine'a Bağışta Bulunun", + "onlyOnce": "(Bu mesaj size yalnızca bir defa gösterilecektir. Eğer gelecekte Twine'ın geliştirilmesi için bağışta bulunmak isterseniz Twine Hakkında iletişim kutusunda ilgili bağlantı mevcuttur.)", + "supportMessage": "Eğer Twine'ı çok sevdiyseniz lütfen geliştirilmesi için bağışta bulunarak yardım edin. Twine daima bedava kalacak bir açık kaynak projesidir, ve sizin de desteğinizle daha iyi yerlere gelecektir.", + "noThanks": "Hayır, Teşekkürler", + "title": "Twine'ın Geliştirilmesine Katkıda Bulunun" }, - "appDonation": {"noThanks": "Hayır, Teşekkürler"}, - "appPrefs": {"language": "Dil"}, - "passageEdit": {}, - "passageTags": {}, - "storyInfo": {"stats": {"title": "Öykü İstatistikleri"}}, + "appPrefs": { + "codeEditorFont": "Kod Düzenleyici Yazı Tipi", + "codeEditorFontScale": "Kod Düzenleyici Yazı Boyutu", + "dialogWidth": "İletişim Kutusu Eni", + "dialogWidths": { + "default": "Varsayılan", + "wider": "Daha Geniş", + "widest": "En Geniş" + }, + "editorCursorBlinks": "Düzenleyicide Yanıp Sönen İmleç", + "fontExplanation": "Yazı tipini burada değiştirmek yalnızca Twine düzenleyicsini etkiler. Oynatılan öykünün yazı tipi değiştirilmez.", + "language": "Dil", + "passageEditorFont": "Bölüm Düzenleyici Yazı Tipi", + "passageEditorFontScale": "Bölüm Düzenleyici Yazı Boyutu", + "themeLight": "Açık", + "themeDark": "Koyu", + "themeSystem": "Sistem", + "theme": "Tema", + "title": "Tercihler" + }, + "passageEdit": { + "editorCrashed": "Bu düzenleyicide bir şeyler ters gitti. Kapatıp yeniden açmayı deneyin.", + "passageTextEditorLabel": "Bölüm Metni", + "passageTextPlaceholder": "Bölümün metnini buraya yazın. Başka bir bölüme bağlantı oluşturmak için [[bunun gibi]] etrafına ikişer köşeli parantez koyun.", + "setAsStart": "Öyküyü Buradan Başlat", + "size": "Boyut", + "sizeLarge": "Büyük", + "sizeSmall": "Küçük", + "sizeTall": "Uzun", + "sizeWide": "Geniş" + }, + "passageTags": { + "noTags": "Bu öyküde hiçbir bölüme bir etiket eklenmedi.", + "title": "Bölüm Etiketleri" + }, + "storyImport": { + "deselectAll": "Seçimi Kaldır", + "fileImport": "Twine'a öykü aktarmak için arşivlenmiş ya da yayımlanmış bir öykü dosyasını aşağıdan yükleyin.", + "importDifferentFile": "Başka Bir Dosyayı İçe Aktar", + "importSelected": "Seçili Dosyaları İçe Aktar", + "importThisStory": "Bu Öyküyü İçe Aktar", + "noStoriesInFile": "Bu dosyada hiç Twine öyküsü yokmuş gibi görünüyor. Lütfen başka bir dosya seçin.", + "storiesPrompt": "Hangi öykülerin içe aktarılacağını seçin:", + "title": "Öykü İçe Aktar", + "willReplaceExisting": "Kitaplığınızdaki aynı isimli bir öykü yerine yenisi konacaktır." + }, + "storyDetails": { + "storyFormatExplanation": "Öykü biçimi nedir?", + "snapToGrid": "Kılavuza Uydur", + "stats": { + "brokenLinks": "Kırık Bağlantı", + "characters": "Karakter", + "title": "Öykü İstatistikleri", + "ifid": "Bu öykünün IFID'i {{ifid}}", + "ifidExplanation": "IFID nedir?", + "lastUpdate": "Bu öykü en son {{date}} tarihinde düzenlendi.", + "links": "Bağlantı", + "passages": "Bölüm", + "words": "Sözcük" + } + }, + "storyInfo": { "stats": {"title": "Öykü İstatistikleri"}}, "storyJavaScript": { - "explanation": "Buraya girdiğiniz JavaScript kodu; öykü bir İnternet tarayıcısında açıldığında istisnasız çalıştırılacaktır." + "editorLabel": "Öykü JavaScript'i", + "title": "Öykü JavaScript'i", + "explanation": "Buraya girdiğiniz JavaScript kodu; öykü bir İnternet tarayıcısında açıldığında anında çalıştırılacaktır." }, "storySearch": { "title": "Bul ve Değiştir", - "replaceWith": "Şununla Değiştir" + "find": "Bul", + "includePassageNames": "Bölüm Başlıklarını Dahil Et", + "matchCase": "Büyük Küçük Harf Eşleştir", + "matchCount": "{{count}} eşleşen bölüm", + "matchCount_plural": "{{count}} eşleşen bölüm", + "replaceAll": "Tüm Bölümlerde Değiştir", + "replaceWith": "Şununla Değiştir", + "useRegexes": "Düzenli İfade (RegEx) Kullan" }, "storyStylesheet": { + "editorLabel": "Öykü Stil Şablonu", + "title": "Öykü Stil Şablonu", "explanation": "Buraya girdiğiniz CSS kodu öykünün varsayılan görünümünü değiştirir." }, - "storyTags": {} + "storyTags": { + "noTags": "Öykülerinize hiçbir etiket eklenmedi.", + "title": "Öykü Etiketleri" + } }, "electron": { - "errors": {"storyFileChangedExternally": {}}, - "menuBar": {"edit": "Düzen"}, - "storiesDirectoryName": "Öyküler" + "backupsDirectoryName": "Yedekler", + "errors": { + "jsonSave": "Ayarlar dosyasını kaydederken bir hata oluştu.", + "storyFileChangedExternally": { + "message": "Kitaplığınızdaki “{{fileName}}” isimli dosya Twine'ın dışında düzenlenmiş.", + "detail": "Değişikliklerinizi kaydetmek bu dosyanın üstüne yazar. Eğer Twine'a şuan yüklü olan sürümü değil, dosyadaki sürümü kullanmak isterseniz Twine yeniden başlayacak ve değişiklikleriniz dosyadakilerle değiştirilecektir.", + "overwriteChoice": "Twine'daki Değişiklikleri Kaydet.", + "relaunchChoice": "Dosyayı Kullan ve Yeniden Başlat" + }, + "storyDelete": "Öykü silinirken bir şeyler ters gitti.", + "storyRename": "Öykü yeniden adlandırılırken bir hata oluştu.", + "storySave": "Öykü kaydedilirken bir hata oluştu." + }, + "menuBar": { + "checkForUpdates": "Güncellemeleri Kontrol Et...", + "edit": "Düzen", + "showDevTools": "Hata Ayıklama Konsolunu Göster", + "showStoryLibrary": "Öykü Kitaplığını Göster", + "speech": "Konuşma", + "troubleshooting": "Hata Giderme", + "twineHelp": "Twine Yardımı", + "view": "Görünüm" + }, + "storiesDirectoryName": "Öyküler", + "updateCheck": { + "download": "İndir", + "error": "Twine'ın güncellemeleri kontrol edilirken bir şeyler ters gitti.", + "updateAvailable": "Twine'ın yeni sürümü mevut.", + "upToDate": "Twine'ın mevcut olan en yeni sürümünü kullanmaktasınız." + } }, "routes": { "storyEdit": { + "toolbar": { + "findAndReplace": "Bul ve Değiştir", + "javaScript": "JavaScript", + "passageTags": "Bölüm Etiketleri", + "snapToGrid": "Kılavuza Uydur", + "startStoryHere": "Öyküye Buradan Başla", + "stylesheet": "Stil Şablonu", + "testFromHere": "Buradan Dene" + }, "topBar": { "addPassage": "Bölüm", "editJavaScript": "Öykünün JavaScript kodunu düzenle", "editStylesheet": "Öykünün Stil Şablonunu Düzenle", "findAndReplace": "Bul ve Değiştir", + "passageTags": "Bölüm Etiketlerini Düzenle", "proofStory": "Yazım Denetleme Nüshasına Bak", - "publishToFile": "Dosyaya Yayımla" + "publishToFile": "Dosyaya Yayımla", + "selectAllPassages": "Tüm Bölümleri Seç" + }, + "zoomButtons": { + "storyStructure": "Yalnızca Öykü Yapısını Göster", + "passageNames": "Yalnızca Bölüm Adlarını Göster", + "passageNamesAndExcerpts": "Bölüm Adlarını ve İçeriğin Kısımlarını Göster" } }, "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Öykü biçimleri hikayenin okuma sırasında nasıl göründüğünü ve davrandığını belirler." + "noneVisible": "Seçtiğiniz kriterlere uygun öykü biçimi bulunmamaktadır.", + "show": "Show...", + "title": { + "all": "Tüm Öylü Biçimleri", + "current": "Güncel Öykü Biçimleri", + "user": "Kendinizce Eklenen Öykü Biçimleri" + }, + "toolbar": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}} eklenecek.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} zaten ekli.", + "fetchError": "Bu adresteki öykü biçimi elde edilemedi. ({{errorMessage}})", + "invalidUrl": "Lütfen geçerli bir URL girin.", + "prompt": "Öykü biçimi eklemek için biçimin internet adresini aşağı girin." + }, + "disableFormatExtensions": "Düzenleyici Eklentilerini Kapat", + "enableFormatExtensions": "Düzenleyici Eklentilerini Aç", + "useAsDefaultFormat": "Varsayılan Biçim Olarak Kullan", + "useAsProofingFormat": "Öykü İmlası için Kullan" + }, + "storyFormatExplanation": "Öykü biçimleri öykünün okuma sırasında nasıl göründüğünü ve davrandığını belirler." }, - "storyImport": {}, "storyList": { + "library": "Kitaplık", "noStories": "Şu an Twine'da kayıtlı hiçbir öykü yok. Başlamak için yeni bir öykü yaratın veya var olan bir öyküyü içe aktarın.", + "taggedTitleCount": "1 Öykü Etiketli", + "taggedTitleCount_0": "Hiç Öykü Etiketli Değil", + "taggedTitleCount_plural": "{{count}} Öykü Etiketli", + "titleCount": "1 Öykü", + "titleCount_0": "Hiç Öykü Yok", + "titleCount_plural": "{{count}} Öykü", "titleGeneric": "Öyküler", + "toolbar": { + "archive": "Arşivle", + "createStoryButton": { + "prompt": "Öykünün adı ne olsun? Bu daha sonra değiştirilebilir.", + "emptyName": "Lütfen bir ad girin.", + "nameConflict": "Başka bir öykü zaten bu ada sahip." + }, + "deleteStoryButton": { + "warning": { + "electron": "“{{storyName}}” isimli öyküyü silmek istediğinizden emin misiniz? Geri dönüşüm kutusuna taşınacaktır.", + "web": "“{{storyName}}” isimli öyküyü silmek istediğinizden emin misiniz? Sonsuza dek silinecektir. Bunu geri alamazsınız." + } + }, + "showAllStories": "Tüm Öyküleri Göster", + "showTags": "Etiketleri Göster", + "sort": "Sırala", + "sortByDate": "Son Değiştirme Tarihi", + "sortByName": "Ad", + "storyTags": "Öykü Etiketleri" + }, "topBar": { "about": "Twine Hakkında", "archive": "Arşiv", @@ -93,21 +367,61 @@ } }, "welcome": { + "autosave": "

Artık belgelerim klasörünüzde Twine isimli bir klasör var. Onun içinde tüm çalışmalarınızın kaydedileceği \"Stories\" (Öyküler) klasörü bulunmaktadır. Twine çalışmalarınız kaydeder, yani kendinizin kaydetmeyi hatırlaması gerekmez. Öykülerinizin kaydedildiği klasörü açmak için Twine menüsündeki Kitaplığı Göster seçeneğini kullanabilirsiniz.

Twine öyküleriniz sürekli kaydettiği için Twine açıkken kitaplık klasörünüzdeki dosyalar kitlenecektir.

Eğer başkasından aldığınız bir Twine öyküsünü açmak istiyorsanız öykü listesindeki Dosyadan İçe Aktar bağlantısını kullanarak kendi kitaplığınıza aktarabilirsiniz.

", "autosaveTitle": "Çalışmalarınız otomatik olarak kaydedilir.", + "browserStorage": "

Bu Twine 2'yi kullanmak için bir hesap oluşturmanıza gerek kalmadığı anlamına gelir ve oluşturduğunuz hiç bir şey uzaktaki bir sunucuda saklanmaz&emdash;yalnızca tarayıcınızda kalır.

Fakat iki çok önemli şeyi aklınızda bulundurmanız gerekmektedir. Verileriniz yalnızca tarayıcınızda saklandığından dolayı tarayıcının kayıtlı verilerini silerseniz tüm emeklerinizi boşa çıkarırsınız. Bu hiç iyi bir durum değil. Arşivle düğmesini sık sık kullanmayı unutmayın. Ayrıca, listedeki öykülerin menüsünü kullanarak onları teker teker yayımlayabilirsiniz. Hem arşiv hem de yayımlanmış öykü dosyaları Twine'a geri aktarılabilir.

İkinci olarak bu tarayıcıya erişimi olan herkes öykülerinizde değişiklikler yapabilir. Eğer işinize karışmayı seven küçük kardeşiniz varsa kendinize ayrı bir profil oluşturmayı araştırın.

", + "browserStorageTitle": "Çalışmalarınız yalnızca tarayıcıya kaydedilmektedir.", + "done": "

Okudunuz için teşekkür eder, Twine'ı kullanırken eğlenmenizi dileriz.

", "doneTitle": "Tamamdır!", "gotoStoryList": "Öykü listesine git", + "greeting": "

Twine, interaktif ve doğrusal olmayan öyküler oluşturmak için kullanılan bir açık kaynaklı yazılımdır. Başlamadan önce bilmeniz gerekn bazı şeyler vardır.

", "greetingTitle": "Merhaba!", "tellMeMore": "Devam Et", + "help": "

Eğer daha önce Twine'ı kullanmadıysanız hoş geldiniz! Twine Cookbok nasıl kullanıldığını öğrenmek için iyi bir kaynaktır. Eğer Twine'ı daha önce kullanmadıysanız başlamak için harika bir yer.

", "helpTitle": "Yeni misiniz?" } }, + "routeActions": { + "app": { + "aboutApp": "Twine Hakkında", + "preferences": "Tercihler", + "reportBug": "Hata Bildir", + "storyFormats": "Öykü Biçimleri" + }, + "build": { + "play": "Oynat", + "proof": "İmla Düzelt", + "publishToFile": "Dosyaya Yayımla", + "test": "Dene" + } + }, "store": { - "errors": {}, + "archiveFilename": "{{timestamp}} Twine Arşivi.html", + "errors": { + "cantPersistPerfs": "Tercihlerinizi kaydederken bir şeyler ters gitti ({{error}}).", + "cantPersistStories": "Öyküleriniz kaydederken bir şeyler ters gitti ({{error}}).", + "cantPersistStoryFormats": "Öykü biçimleriniz kaydederken bir şeyler ters gitti ({{error}}).", + "electronRemediation": "Uygulamayı yeniden başlatmak yardımcı olabilir.", + "webRemediation": "Sayfayı yenilemek yardımcı olabilir." + }, "passageDefaults": { "name": "Adsız Bölüm" }, "storyDefaults": {"name": "Adsız Öykü"}, "storyFormatDefaults": {"name": "Adsız Öykü Biçimi"} }, - "undoChange": {"replaceAllText": "Hepsini Değiştir"} -} + "undoChange": { + "addTag": "Etiket Ekleme", + "changeTagColor": "Etiket Rengini Değiştirme", + "newPassage": "Yeni Bölüm", + "deletePassage": "Bölüm Silme", + "deletePassages": "Bölümleri Silme", + "movePassage": "Bölüm Taşıma", + "movePassages": "Bölümleri Taşıma", + "imortTag": "Etiket Kaldırma", + "renamePassage": "Bölüm Yeniden Adlandırma", + "removeTag": "Etiketi Kaldırma", + "renameTag": "Etiketi Yeniden Adlandırma", + "replaceAllText": "Hepsini Değiştirme" + } +} \ No newline at end of file diff --git a/public/locales/uk.json b/public/locales/uk.json index cce00cb19..4af45cfa2 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -1,107 +1,424 @@ { - "colors": {}, + "colors": { + "none": "Немає", + "red": "Червоний", + "orange": "Помаранчевий", + "yellow": "Жовтий", + "green": "Зелений", + "blue": "Синій", + "purple": "Фіолетовий" + }, "common": { "add": "Додати", "appName": "Twine", + "back": "Назад", + "build": "Запуск", "cancel": "Скасувати", + "close": "Закрити", + "color": "Колір", + "create": "Створити", + "custom": "Custom", "delete": "Видалити", + "deleteCount": "Видалити ({{count}})", + "details": "Деталі", "duplicate": "Дублювати", - "edit": "Редаґувати", + "edit": "Редагувати", + "editCount": "Редагувати ({{count}})", + "help": "Допомога", + "import": "Імпорт", + "maximize": "На весь екран", + "more": "Більше", + "new": "Створити", + "next": "Далі", "ok": "OK", + "passage": "Параграф", "play": "Відтворити", + "preferences": "Налаштування", + "publishToFile": "Публікувати у файл", + "redo": "Повторити", + "redoChange": "Повторити {{change}}", "rename": "Перейменувати", - "remove": "Усунути", + "renamePrompt": "Яка нова назва для “{{name}}”?", + "remove": "Видалити", + "selectAll": "Обрати всі", "skip": "Пропустити", - "storyFormat": "Формат розповіді", + "story": "Оповідання", + "storyFormat": "Формат оповідання", "tag": "Мітка", - "test": "Тестувати", - "undo": "Відмінити" + "test": "Тест", + "twine": "Twine", + "undo": "Скасувати", + "undoChange": "Скасувати {{change}}", + "unmaximize": "Відновити розмір", + "view": "Вигляд" }, "components": { - "addStoryFormatButton": {}, - "addTagButton": {}, - "fontSelect": {"fonts": {}}, - "indentButtons": {}, - "localStorageQuota": {}, + "addTagButton": { + "alreadyAdded": "Така мітка вже існує.", + "addLabel": "Додати мітку", + "invalidName": "Введіть коректну назву мітки.", + "newTag": "Нова мітка", + "tagColorLabel": "Колір мітки", + "tagNameLabel": "Назва мітки" + }, + "dialogCard": { + "contentsCrashed": "Щось пішло не так. Спробуйте закрити це діалогове вікно та відкрити його знову." + }, + "fontSelect": { + "customScaleDetail": "Введіть тільки проценти.", + "customFamilyDetail": "Введіть тільки назву шрифту.", + "familyEmpty": "Введіть назву шрифту.", + "font": "Шрифт", + "fonts": { + "monospaced": "Моноширинний", + "serif": "Із засічками (serif)", + "system": "Системний" + }, + "fontSize": "Розмір шрифту", + "percentage": "{{percent}}%", + "percentageIsntNumber": "Введіть число.", + "percentageNotPositive": "Введіть число більше за 0." + }, + "indentButtons": { + "indent": "Збільшити відступ", + "unindent": "Зменшити відступ" + }, + "localStorageQuota": { + "measureAgain": "Перевірити об'єм доступного місця", + "percentAvailable": "{percent}% місця доступно" + }, "passageCard": { - "placeholderClick": "Клацніть двічі на уривок для редаґування.", - "placeholderTouch": "Натисність на цей уривок, а потім на олівець, аби почати редаґувати." - }, - "renamePassageButton": {"emptyName": "Будь ласка, введіть назву."}, - "renameStoryButton": {"emptyName": "Будь ласка, введіть назву."}, - "safariWarningCard": {}, - "storyCard": {}, - "storyFormatCard": {}, - "storyFormatSelect": {}, - "tagEditor": {} + "placeholderClick": "Клацніть двічі цей параграф, щоб редагувати його.", + "placeholderTouch": "Натисніть на цей параграф, потім оберіть “Редагувати” з вкладки “Параграфи”, щоб редагувати його." + }, + "renamePassageButton": { + "emptyName": "Введіть назву.", + "nameAlreadyUsed": "Інший параграф в оповіданні вже має таку назву." + }, + "renameStoryButton": { + "emptyName": "Введіть назву.", + "nameAlreadyUsed": "Інше оповідання вже має таку назву." + }, + "safariWarningCard": { + "archiveAndUseAnotherBrowser": "Архівуйте ваші оповідання та використовуйте інший браузер.", + "addToHomeScreen": "Додайте цей сайт до вашого домашнього екрану, щоб зняти це обмеження.", + "howToAddToHomeScreen": "Як додати сайт на домашній екран?", + "learnMore": "Дізнатися більше", + "message": "Ваш браузер видалить всі ваші оповідання, якщо ви не відвідували цей сайт протягом семи днів." + }, + "storageQuota": { + "freeSpace": "{{percent}}% місця доступно" + }, + "storyCard": { + "lastUpdated": "Дата редагування: {{date}}", + "passageCount": "1 параграф", + "passageCount_1": "{{count}} параграфи", + "passageCount_2": "{{count}} параграфів" + }, + "storyFormatCard": { + "author": "Автор: {{author}}", + "builtIn": "Вбудований", + "defaultFormat": "Використовується за замовчуванням", + "editorExtensionsDisabled": "Розширення редактора вимкнено", + "license": "Ліцензія: {{license}}", + "loadingFormat": "Завантаження формату оповідання...", + "loadError": "Цей формат оповідання не вийшло завантажити: ({{errorMessage}}).", + "name": "{{name}} {{version}}", + "proofing": "Вичитка", + "proofingFormat": "Використовується для вичитки", + "useEditorExtensions": "Використовувати розширення редактора", + "useFormat": "Використовувати як формат оповідання за замовчуванням", + "useProofingFormat": "Використовувати як формат для вичитки" + }, + "storyFormatSelect": { + "loadingCount": "Завантажується формат оповідання...", + "loadingCount_1": "Завантажуються {{loadingCount}} формати оповідання...", + "loadingCount_2": "Завантажується {{loadingCount}} форматів оповідання..." + }, + "tagEditor": { + "alreadyExists": "Мітка з такою назвою вже існує." + } }, "dialogs": { "aboutTwine": { - "donateToTwine": "Ваш внесок допоможе Twine’ові розвиватися" + "donateToTwine": "Зробити внесок на розвиток Twine", + "codeHeader": "Код", + "codeRepo": "Репозиторій вихідного коду", + "license": "Цей додаток випущено за ліцензією GPL v3, проте будь-які роботи, створені за його допомогою, можуть бути випущені на будь-яких умовах, включаючи комерційні.", + "localizationHeader": "Локалізації", + "title": "Про Twine {{version}}", + "twineDescription": "Twine - це інструмент з відкритим кодом для створення інтерактивних нелінійних оповідань." + }, + "appDonation": { + "donate": "Зробити внесок на розробку Twine", + "onlyOnce": "(Це повідомлення буде показано вам лише один раз. Якщо ви захочете зробити внесок на розробку Twine пізніше, перейдіть за посиланням у вікні “Про Twine”.)", + "supportMessage": "Якщо вам подобається Twine, можливо, ви захочете допомогти його розвитку фінансовим внеском. Twine - це проєкт з відкритим кодом, який завжди буде безкоштовним, а з вашою допомогою Twine продовжить розвиватися.", + "noThanks": "Ні, дякую", + "title": "Підтримати розробку Twine" + }, + "appPrefs": { + "codeEditorFont": "Шрифт редактора", + "codeEditorFontScale": "Розмір шрифту редактора", + "dialogWidth": "Ширина діалогу", + "dialogWidths": { + "default": "Стандартна", + "wider": "Ширша", + "widest": "Найширша" + }, + "editorCursorBlinks": "Блимаючий курсор в редакторі", + "fontExplanation": "Ці шрифти використовуються лише в редакторі Twine. Вони не впливають на шрифт, який використовується під час відтворення оповідання.", + "language": "Мова", + "passageEditorFont": "Шрифт редактора параграфів", + "passageEditorFontScale": "Розмір шрифта редактора параграфів", + "themeLight": "Світла", + "themeDark": "Темна", + "themeSystem": "Системна", + "theme": "Тема оформлення", + "title": "Налаштування" + }, + "passageEdit": { + "editorCrashed": "Щось не так з редактором. Спробуйте його закрити та відкрити знову.", + "passageTextEditorLabel": "Текст параграфа", + "passageTextPlaceholder": "Введіть текст параграфа тут. Щоб створити посилання на параграф, візьміть його назву у дві квадратні дужки, [[ось так]].", + "setAsStart": "Почати оповідання тут", + "size": "Розмір", + "sizeLarge": "Великий", + "sizeSmall": "Маленький", + "sizeTall": "Високий", + "sizeWide": "Широкий" + }, + "passageTags": { + "noTags": "У параграфів в цьому оповіданні немає міток.", + "title": "Мітки параграфів" + }, + "storyImport": { + "deselectAll": "Зняти виділення", + "filePrompt": "Ви можете імпортувати оповідання в Twine з файлу архіву або опублікованого оповідання.", + "importDifferentFile": "Імпортувати інший файл", + "importSelected": "Імпортувати обрані файли", + "importThisStory": "Імпортувати це оповідання", + "noStoriesInFile": "Цей файл не є оповіданням Twine. Оберіть інший файл.", + "storiesPrompt": "Оберіть оповідання для імпорту:", + "title": "Імпорт оповідань", + "willReplaceExisting": "Оповідання з такою ж назвою в вашій бібліотеці буде замінене." + }, + "storyDetails": { + "storyFormatExplanation": "Що таке формат оповідання?", + "snapToGrid": "Прив'язка до сітки", + "stats": { + "brokenLinks": "зламаних посилань", + "characters": "символів", + "title": "Статистика оповідання", + "ifid": "IFID цього оповідання: {{ifid}}.", + "ifidExplanation": "Що таке IFID?", + "lastUpdate": "Дата та час останнього редагування: {{date}}.", + "links": "посилань", + "passages": "параграфів", + "words": "слів" + } }, - "appDonation": {"noThanks": "Ні, дякую"}, - "appPrefs": {"language": "Мова"}, - "passageEdit": {}, - "passageTags": {}, - "storyInfo": {"stats": {"title": "Статистика розповіді"}}, "storyJavaScript": { - "explanation": "Будь-який введений сюди JavaScript негайно розпочне діяти, як тільки Ваша розповідь буде відкрита у вебоглядачеві." + "editorLabel": "JavaScript-код оповідання", + "title": "JavaScript-код оповідання", + "explanation": "Цей код JavaScript буде негайно виконаний, коли це оповідання буде відкрито в браузері." + }, + "storySearch": { + "title": "Знайти і замінити", + "find": "Знайти", + "includePassageNames": "Включати назви параграфів", + "matchCase": "Співпадіння за регістром", + "matchCount": "Знайдено {{count}} параграф", + "matchCount_1": "Знайдено {{count}} параграфи", + "matchCount_2": "Знайдено {{count}} параграфів", + "noMatches": "Немає підходящих параграфів", + "replaceAll": "Замінити у всіх параграфах", + "replaceWith": "Замінити на", + "useRegexes": "Використовувати регулярні вирази" }, - "storySearch": {"title": "Знайти й Замінити", "replaceWith": "Замінити"}, "storyStylesheet": { - "explanation": "Будь-які введені сюди CSS змінять вигляд-за-замовчуванням Вашої розповіді." + "editorLabel": "Таблиця стилів оповідання", + "title": "Таблиця стилів оповідання", + "explanation": "Стилі в цій CSS-таблиці перевизначають стилі оповідання за замовчуванням, змінюючи його вигляд." }, - "storyTags": {} + "storyTags": { + "noTags": "Немає оповідань з мітками.", + "title": "Мітки оповідань" + } }, "electron": { - "errors": {"storyFileChangedExternally": {}}, - "menuBar": {"edit": "Редаґувати"}, - "storiesDirectoryName": "Розповіді" + "backupsDirectoryName": "Backups", + "errors": { + "jsonSave": "Щось пішло не так під час збереження файлу налаштувань.", + "storyFileChangedExternally": { + "message": "Файл “{{fileName}}” з вашої бібліотеки оповідань було змінено за межами Twine.", + "detail": "Збереження змін перезапише цей файл. Якщо ви хочете використовувати цей файл замість того, що є в Twine, Twine буде перезавантажено і ваші зміни будуть знищені.", + "overwriteChoice": "Зберегти зміни з Twine", + "relaunchChoice": "Завантажити файл та перезапустити" + }, + "storyDelete": "Щось пішло не так під час видалення оповідання.", + "storyRename": "Щось пішло не так під час зміни назви оповідання.", + "storySave": "Щось пішло не так під час збереження оповідання." + }, + "menuBar": { + "checkForUpdates": "Перевірити оновлення...", + "edit": "Редагувати", + "showDevTools": "Показати консоль відлагодження", + "showStoryLibrary": "Показати бібліотеку оповідань", + "speech": "Speech", + "troubleshooting": "Вирішення проблем", + "twineHelp": "Допомога по Twine", + "view": "Вигляд" + }, + "storiesDirectoryName": "Оповідання", + "updateCheck": { + "download": "Завантажити", + "error": "Щось пішло не так під час перевірки оновлень Twine.", + "updateAvailable": "Доступна новіша версія Twine.", + "upToDate": "Це остання версія Twine." + } }, "routes": { "storyEdit": { + "toolbar": { + "findAndReplace": "Знайти та замінити", + "javaScript": "JavaScript", + "passageTags": "Мітки параграфів", + "snapToGrid": "Прив'язка до сітки", + "startStoryHere": "Почати оповідання тут", + "stylesheet": "Таблиця стилів", + "testFromHere": "Почати перевірку тут" + }, "topBar": { - "addPassage": "Уривок", - "editJavaScript": "Редаґувати JavaScript розповіді", - "editStylesheet": "Редаґувати стильову таблицю розповіді", - "findAndReplace": "Знайти й Замінити", - "proofStory": "Дивитися Коректурну копію", - "publishToFile": "Опублікувати як файл" + "editJavaScript": "Редагувати JavaScript оповідання", + "editStylesheet": "Редагувати таблицю стилів оповідання", + "findAndReplace": "Знайти та замінити", + "passageTags": "Редагувати мітки параграфів", + "proofStory": "Вичитка", + "publishToFile": "Публікувати у файл", + "selectAllPassages": "Виділити всі параграфи" + }, + "zoomButtons": { + "storyStructure": "Показати лише структуру оповідання", + "passageNames": "Показати лише назви параграфів", + "passageNamesAndExcerpts": "Показати назви параграфів та уривки" } }, "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Формати розповідей визначають їхній вигляд і поведінку під час відтворення." + "noneVisible": "Немає форматів оповідання, що підходять під обрані вами критерії.", + "show": "Показати...", + "title": { + "all": "Всі формати оповідань", + "current": "Формати цього оповідання", + "user": "Формати, додані користувачами" + }, + "toolbar": { + "addStoryFormatButton": { + "addPreview": "Буде додано формат {{storyFormatName}} {{storyFormatVersion}}.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} вже було додано.", + "fetchError": "Не вийшло завантажити формат оповідання за цією адресою ({{errorMessage}}).", + "invalidUrl": "Введіть коректний URL.", + "prompt": "Щоб додати формат оповідання, введіть його адресу." + }, + "disableFormatExtensions": "Вимкнути розширення редактора", + "enableFormatExtensions": "Ввімкнути розширення редактора", + "useAsDefaultFormat": "Використовувати як формат за замовчуванням", + "useAsProofingFormat": "Використовувати для вичитки" + }, + "storyFormatExplanation": "Формат оповідання контролює вигляд та поведінку оповідань під час відтворення." }, - "storyImport": {}, "storyList": { - "noStories": "Поки Twine не має жодних збережених розповідей. Аби розпочати, створіть нову розповідь чи зімпортуйте вже існуючу з файлу.", - "titleGeneric": "Розповіді", - "topBar": { - "about": "Про Twine", + "library": "Бібліотека", + "noStories": "Зараз в Twine немає оповідань. Ви можете створити нове оповідання або імпортувати вже існуюче з файлу.", + "taggedTitleCount": "1 оповідання з мітками", + "taggedTitleCount_0": "Немає оповідань з мітками", + "taggedTitleCount_1": "{{count}} оповідання з мітками", + "taggedTitleCount_2": "{{count}} оповідань з мітками", + "titleCount": "1 оповідання", + "titleCount_0": "Немає оповідань", + "titleCount_1": "{{count}} оповідання", + "titleCount_2": "{{count}} оповідань", + "titleGeneric": "Оповідання", + "toolbar": { "archive": "Архів", - "createStory": "Розповідь", - "help": "Допомога", - "sortName": "Назва", - "storyFormats": "Формати" + "createStoryButton": { + "prompt": "Яка назва буде у вашого оповідання? Ви можете змінити її пізніше.", + "emptyName": "Введіть назву.", + "nameConflict": "Вже є оповідання з такою назвою." + }, + "deleteStoryButton": { + "warning": { + "electron": "Ви впевнені, що хочете видалити “{{storyName}}”? Воно буде переміщено у корзину.", + "web": "Ви впевнені, що хочете видалити “{{storyName}}”? Воно буде видалено назавжди. Цю дію не можна буде скасувати." + } + }, + "showAllStories": "Показати всі оповідання", + "showTags": "Показати мітки", + "sort": "Сортування", + "sortByDate": "Дата оновлення", + "sortByName": "Назва", + "storyTags": "Мітки" } }, "welcome": { - "autosaveTitle": "Вашу роботу автоматично збережено.", - "doneTitle": "Ось і все!", - "gotoStoryList": "Перейти до Списку розповідей", + "autosave": "

В папці ваших документів тепер є папка “Twine”. Всередині є папка “Stories”, де буде зберігатися вся ваша праця. Twine зберігає все автоматично під час роботи, тому вам не треба хвилюватися про це. Ви можете відкрити папку з вашими оповіданнями за допомогою пункту “Показати бібліотеку” в меню “Twine”.

Оскільки Twine постійно зберігає вашу працю, файли в вашій бібліотеці оповідань будуть заблоковані для редагування, поки Twine відкритий.

Якщо ви хочете відкрити файл оповідання Twine, який отримали від когось іншого, ви можете імпортувати його в вашу бібліотеку за допомогою функції “Імпорт” в переліку оповідань.

", + "autosaveTitle": "Ваша праця автоматично зберігається.", + "browserStorage": "

Вам не потрібно створювати обліковий запис, щоб використовувати Twine 2. Все, що ви створюєте, зберігається не на якомусь сервері, а залишається в вашому браузері.

Зверніть увагу на дві дуже важливі речі. По-перше, оскільки ваша робота зберігається лише в вашому браузері, ви втратите її, якщо видалите з браузеру збережені дані! Погано. Тому використовуйте функцію “Архів” частіше. Також ви можете публікувати окремі оповідання в файл, використовуючи меню в списку оповідань. Архіви та файлі оповідань завжди можна знову імпортувати в Twine.

По-друге, будь-який користувач цього браузеру може бачити та змінювати вашу роботу. Якщо у вас є допитливий молодший брат, краще створіть окремий профіль для себе.

", + "browserStorageTitle": "Ваша праця зберігається лише в вашому браузері", + "done": "

Дякуємо за читання. Гарно провести час з Twine!

", + "doneTitle": "Це все!", + "gotoStoryList": "Перейти до списку оповідань", + "greeting": "

Twine - це інструмент з відкритим кодом для створення інтерактивних нелінійних оповідань. Є декілька речей, які потрібно знати перед тим, як почати.

", "greetingTitle": "Привіт!", - "tellMeMore": "Розкажіть мені більше", - "helpTitle": "Потрібна допомога?" + "tellMeMore": "Дізнатися більше", + "help": "

Якщо ви не використовували Twine раніше, ласкаво просимо! Посібник Twine (англійською) допоможе навчитися ним користуватися. Якщо ви не користувалися Twine раніше, радимо почати з нього.

", + "helpTitle": "Новенький?" + } + }, + "routeActions": { + "app": { + "aboutApp": "Про Twine", + "preferences": "Налаштування", + "reportBug": "Сповістити про помилку", + "storyFormats": "Формати оповідання" + }, + "build": { + "play": "Відтворити", + "proof": "Вичитати", + "publishToFile": "Публікувати у файл", + "test": "Тестувати" } }, "store": { - "errors": {}, + "archiveFilename": "{{timestamp}} Twine Archive.html", + "errors": { + "cantPersistPrefs": "Щось пішло не так під час збереження налаштувань ({{error}}).", + "cantPersistStories": "Щось пішло не так під час збереження оповідань ({{error}}).", + "cantPersistStoryFormats": "Щось пішло не так під час збереження форматів оповідання ({{error}}).", + "electronRemediation": "Перезапуск цього додатку може допомогти.", + "webRemediation": "Перезавантаження цієї сторінки може допомогти." + }, "passageDefaults": { - "name": "Уривок без назви" + "name": "Параграф без назви" }, - "storyDefaults": {"name": "Розповідь без назви"}, - "storyFormatDefaults": {"name": "Формат розповіді без назви"} + "storyDefaults": { + "name": "Оповідання без назви" + }, + "storyFormatDefaults": { + "name": "Формат оповідання без назви" + } }, - "undoChange": {"replaceAllText": "Замінити все"} + "undoChange": { + "addTag": "додавання мітки", + "changeTagColor": "зміну кольору мітки", + "newPassage": "створення параграфа", + "deletePassage": "видалення параграфа", + "deletePassages": "видалення параграфів", + "movePassage": "переміщення параграфа", + "movePassages": "переміщення параграфів", + "imortTag": "видалення мітки", + "renamePassage": "перейменування параграфа", + "removeTag": "видалення мітки", + "renameTag": "перейменування мітки", + "replaceAllText": "заміну тексту" + } } diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index 8acf0aae6..d4508f661 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -9,45 +9,48 @@ "purple": "紫色" }, "common": { + "add": "添加", + "appName": "Twine", "back": "返回", "build": "构建", + "cancel": "取消", "close": "关闭", "color": "颜色", + "create": "创建", "custom": "自定义", + "delete": "删除", + "deleteCount": "删除({{count}})", "details": "细节", + "duplicate": "复制", + "edit": "编辑", "editCount": "编辑({{count}})", - "deleteCount": "删除({{count}})", "help": "帮助", "import": "导入", + "maximize": "最大化", "more": "更多", "new": "新建", "next": "下一个", + "ok": "好的", "passage": "片段", + "play": "运行", "preferences": "偏好设置", "publishToFile": "发布到文件", "redo": "恢复", "redoChange": "恢复 {{change}}", - "renamePrompt": "您想要将“{{name}}”重命名为什么?", - "selectAll": "全选", - "story": "故事", - "twine": "Twine", - "undoChange": "撤销 {{change}}", - "view": "查看", - "add": "添加", - "appName": "Twine", - "cancel": "取消", - "delete": "删除", - "duplicate": "复制", - "edit": "编辑", - "ok": "好的", - "play": "运行", "rename": "重命名", + "renamePrompt": "您想要将“{{name}}”重命名为什么?", "remove": "移除", + "selectAll": "全选", "skip": "跳过", + "story": "故事", "storyFormat": "故事格式", "tag": "标签", "test": "测试", - "undo": "撤销" + "twine": "Twine", + "undo": "撤销", + "undoChange": "撤销 {{change}}", + "unmaximize": "还原大小", + "view": "查看" }, "components": { "addTagButton": { @@ -58,9 +61,12 @@ "tagColorLabel": "标签颜色", "tagNameLabel": "标签名称" }, + "dialogCard": { + "contentsCrashed": "这一对话似乎遇到了错误。尝试关闭它并重新打开。" + }, "fontSelect": { - "customFamilyDetail": "请只输入字体名称。", "customScaleDetail": "请只输入百分比数值。", + "customFamilyDetail": "请只输入字体名称。", "familyEmpty": "请输入字体名称。", "font": "字体", "fonts": { @@ -73,125 +79,115 @@ "percentageIsntNumber": "请输入数字。", "percentageNotPositive": "请输入大于 0 的数字。" }, + "indentButtons": { + "indent": "缩进", + "unindent": "无缩进" + }, + "localStorageQuota": { + "measureAgain": "再次检测可用空间", + "percentAvailable": "{percent}% 空间可用" + }, "passageCard": { "placeholderClick": "双击该段落进行编辑。", "placeholderTouch": "点击这个片段,然后点击铅笔图标进行编辑。" }, - "renamePassageButton": { - "nameAlreadyUsed": "此名称已被故事中的另一个片段使用。", - "emptyName": "请输入一个名字。" + "passageFuzzyFinder": { + "noResults": "No matches.", + "prompt": "Search by passage name or text" }, - "storyCard": { - "passageCount": "{{count}}个片段", - "lastUpdated": "最后一次编辑位于 {{date}}", - "passageCount_plural": "{{count}} 个片段" - }, - "indentButtons": { - "indent": "缩进", - "unindent": "无缩进" + "renamePassageButton": { + "emptyName": "请输入一个名字。", + "nameAlreadyUsed": "此名称已被故事中的另一个片段使用。" }, "renameStoryButton": { "emptyName": "请输入一个名字。", "nameAlreadyUsed": "此名称已被另一个故事使用。" }, "safariWarningCard": { - "message": "如果您连续七天没有打开该网站,您正使用的浏览器就将删除所有故事。", "archiveAndUseAnotherBrowser": "请保存故事并改用其他平台。", - "howToAddToHomeScreen": "我该如何把它添加到我的主界面?", "addToHomeScreen": "你可以通过把该网站添加到主界面来避免限制。", - "learnMore": "了解更多" + "howToAddToHomeScreen": "我该如何把它添加到我的主界面?", + "learnMore": "了解更多", + "message": "如果您连续七天没有打开该网站,您正使用的浏览器就将删除所有故事。" }, - "storyFormatSelect": { - "loadingCount_plural": "载入 {{loadingCount}} 个故事格式中……", - "loadingCount": "载入 1 个故事格式中……" + "storageQuota": { + "freeSpace": "{{percent}}% 空间可用" + }, + "storyCard": { + "lastUpdated": "最后一次编辑位于 {{date}}", + "passageCount": "{{count}}个片段", + "passageCount_plural": "{{count}} 个片段" }, "storyFormatCard": { "author": "来自 {{author}}", + "builtIn": "内置", "defaultFormat": "设为默认", + "editorExtensionsDisabled": "编辑器扩展被禁用", "license": "许可证:{{license}}", "loadingFormat": "载入故事格式中……", "loadError": "故事格式加载失败({{errorMessage}})。", "name": "{{name}} {{version}}", - "useFormat": "设为默认故事格式", "proofing": "校对", "proofingFormat": "用于校对", - "useProofingFormat": "设为校对格式", - "editorExtensionsDisabled": "编辑器扩展被禁用", "useEditorExtensions": "使用编辑器扩展", - "builtIn": "内置" + "useFormat": "设为默认故事格式", + "useProofingFormat": "设为校对格式" + }, + "storyFormatSelect": { + "loadingCount": "载入 1 个故事格式中……", + "loadingCount_plural": "载入 {{loadingCount}} 个故事格式中……" }, "tagEditor": { "alreadyExists": "已存在同名标签。" - }, - "localStorageQuota": { - "measureAgain": "再次检测可用空间", - "percentAvailable": "{percent}% 空间可用" - }, - "storageQuota": { - "freeSpace": "{{percent}}% 空间可用" } }, "dialogs": { "aboutTwine": { "donateToTwine": "通过捐赠帮助 Twine 开发", - "license": "本应用程序是在 GPL v3 license 许可证下发布的,以其创建的任何作品都可能以任何条款发布,包括商业应用程序。", "codeHeader": "代码", "codeRepo": "访问源代码库", + "license": "本应用程序是在 GPL v3 license 许可证下发布的,以其创建的任何作品都可能以任何条款发布,包括商业应用程序。", "localizationHeader": "本地化", "title": "关于 Twine {{version}}", "twineDescription": "Twine 是一个用于创作互动的非线性故事的开源工具。" }, "appDonation": { - "noThanks": "不,谢谢", - "supportMessage": "如果你热爱 Twine,请考虑通过捐赠帮助它成长。Twine 是一个永远免费使用的开源项目,在你的帮助下,Twine 将继续蓬勃发展。", "donate": "捐赠给 Twine 开发", "onlyOnce": "(此消息只会显示给您一次。如果您希望将来捐赠给 Twine 开发,您可以在在“关于 Twine”对话框中可以找到相关链接。)", + "supportMessage": "如果你热爱 Twine,请考虑通过捐赠帮助它成长。Twine 是一个永远免费使用的开源项目,在你的帮助下,Twine 将继续蓬勃发展。", + "noThanks": "不,谢谢", "title": "支持 Twine 开发" }, "appPrefs": { - "language": "语言", "codeEditorFont": "代码编辑器字体", "codeEditorFontScale": "代码编辑器字体大小", + "dialogWidth": "对话框宽度", + "dialogWidths": { + "default": "默认", + "wider": "较宽", + "widest": "最宽" + }, "editorCursorBlinks": "代码编辑器中的光标闪烁", "fontExplanation": "此处的字体改变只影响 Twine 编辑器,游玩故事的字体不会发生改变。", + "language": "语言", "passageEditorFont": "片段编辑器字体", "passageEditorFontScale": "片段编辑器字体大小", - "themeLight": "亮", - "themeDark": "暗", + "themeLight": "亮色", + "themeDark": "暗色", "themeSystem": "跟随系统", "theme": "主题", "title": "偏好设置" }, "passageEdit": { + "editorCrashed": "编辑器遇到了一些错误。尝试关闭它并重新编辑这个片段。", + "passageTextEditorLabel": "片段文本", + "passageTextPlaceholder": "在这里输入您的片段的文本。要链接到其他片段需要在它名字前后加两个方括号,[[就像这样]]。", "setAsStart": "从这里开始故事", + "size": "尺寸", "sizeLarge": "大型", "sizeSmall": "小型", "sizeTall": "竖条", - "sizeWide": "横条", - "passageTextEditorLabel": "片段文本", - "size": "形状" - }, - "storyJavaScript": { - "explanation": "当您的故事在Web浏览器中打开时,此处输入的任何JavaScript都将立即运行。", - "editorLabel": "故事 Javascript 脚本", - "title": "故事 Javascript 脚本" - }, - "storySearch": { - "title": "查找替换", - "replaceWith": "替换为", - "includePassageNames": "包含名称", - "matchCase": "匹配大小写", - "matchCount": "{{count}} 个匹配片段", - "matchCount_plural": "{{count}} 个匹配片段", - "noMatches": "没有匹配片段", - "replaceAll": "替换所有片段中的项", - "useRegexes": "使用正则表达式", - "find": "查找" - }, - "storyStylesheet": { - "explanation": "这里输入的任何CSS都会覆盖故事的默认外观。", - "editorLabel": "故事样式表", - "title": "故事样式表" + "sizeWide": "横条" }, "passageTags": { "noTags": "尚未向故事中的片段添加任何标签。", @@ -199,12 +195,12 @@ }, "storyImport": { "deselectAll": "全不选", - "storiesPrompt": "选择要导入的故事:", "filePrompt": "要向 Twine 导入故事,请先在下方上传一个档案或已发布的故事文件。", - "importSelected": "导入选中文件", "importDifferentFile": "导入其他文件", + "importSelected": "导入选中文件", "importThisStory": "导入该故事", "noStoriesInFile": "您上传的文件中似乎没有 Twine 故事。请选择其他文件。", + "storiesPrompt": "选择要导入的故事:", "title": "导入故事", "willReplaceExisting": "将会替换故事库中的同名故事。" }, @@ -212,9 +208,9 @@ "storyFormatExplanation": "故事格式是什么?", "snapToGrid": "对齐到网格", "stats": { - "title": "故事统计", - "brokenLinks": "断开连接", + "brokenLinks": "断开的连接", "characters": "字符", + "title": "故事统计", "ifid": "该故事的 IFID 是 {{ifid}}。", "ifidExplanation": "什么是 IFID?", "lastUpdate": "故事的最后一次变更是在 {{date}}。", @@ -223,67 +219,85 @@ "words": "字" } }, + "storyJavaScript": { + "editorLabel": "故事 Javascript 脚本", + "title": "故事 Javascript 脚本", + "explanation": "当您的故事在Web浏览器中打开时,此处输入的任何JavaScript都将立即运行。" + }, + "storySearch": { + "title": "查找替换", + "find": "查找", + "includePassageNames": "包含名称", + "matchCase": "匹配大小写", + "matchCount": "{{count}} 个匹配片段", + "matchCount_plural": "{{count}} 个匹配片段", + "noMatches": "没有匹配片段", + "replaceAll": "替换所有片段中的项", + "replaceWith": "替换为", + "useRegexes": "使用正则表达式" + }, + "storyStylesheet": { + "editorLabel": "故事样式表", + "title": "故事样式表", + "explanation": "这里输入的任何CSS都会覆盖故事的默认外观。" + }, "storyTags": { "noTags": "尚未向您的故事中添加任何标签。", "title": "故事标签" } }, - "undoChange": { - "replaceAllText": "替换所有", - "changeTagColor": "更改标签颜色", - "newPassage": "新建片段", - "deletePassage": "删除片段", - "deletePassages": "删除片段", - "movePassage": "移动片段", - "movePassages": "移动片段", - "imortTag": "移除标签", - "renamePassage": "重命名片段", - "renameTag": "重命名标签" - }, "electron": { - "menuBar": { - "edit": "编辑", - "twineHelp": "Twine 帮助", - "view": "查看", - "showDevTools": "打开 Debug 控制台", - "showStoryLibrary": "打开故事库", - "troubleshooting": "问题分析", - "speech": "台词" - }, - "storiesDirectoryName": "故事", + "backupsDirectoryName": "备份", "errors": { + "jsonSave": "保存设定文件时发生错误。", "storyFileChangedExternally": { - "detail": "保存更改将会覆盖该文件。如果您想使用该文件而不是 Twine 内的版本, Twine 将会重启,您的作品中的内容会改为使用该文件。", "message": "故事中的文件“{{fileName}}”在 Twine 外被修改。", + "detail": "保存更改将会覆盖该文件。如果您想使用该文件而不是 Twine 内的版本, Twine 将会重启,您的作品中的内容会改为使用该文件。", "overwriteChoice": "保存 Twine 的更改", "relaunchChoice": "使用该文件并重启" }, - "jsonSave": "保存设定文件时发生错误。", "storyDelete": "删除故事时发生错误。", "storyRename": "重命名故事时发生错误。", "storySave": "保存故事时发生错误。" }, - "backupsDirectoryName": "备份" + "menuBar": { + "checkForUpdates": "检查更新……", + "edit": "编辑", + "showDevTools": "打开 Debug 控制台", + "showStoryLibrary": "打开故事库", + "speech": "台词", + "troubleshooting": "问题分析", + "twineHelp": "Twine 帮助", + "view": "查看" + }, + "storiesDirectoryName": "故事", + "updateCheck": { + "download": "下载", + "error": "在检查Twine更新版本时遇到了错误。", + "updateAvailable": "较新版本的Twine现已可用。", + "upToDate": "已经是Twine可用的最新版本。" + } }, "routes": { "storyEdit": { + "toolbar": { + "findAndReplace": "查找替换", + "goTo": "跳转到", + "javaScript": "JavaScript", + "passageTags": "片段标签", + "snapToGrid": "对齐到网格", + "startStoryHere": "从这里开始故事", + "stylesheet": "样式表", + "testFromHere": "从这里开始测试" + }, "topBar": { "editJavaScript": "编辑故事 Javascript 脚本", "editStylesheet": "编辑故事样式表", "findAndReplace": "查找替换", + "passageTags": "编辑片段标签", "proofStory": "查看校对版本", "publishToFile": "发布到文件", - "selectAllPassages": "选择所有片段", - "passageTags": "编辑片段标签" - }, - "toolbar": { - "javaScript": "JavaScript", - "stylesheet": "样式表", - "snapToGrid": "对齐到网格", - "passageTags": "片段标签", - "findAndReplace": "查找替换", - "startStoryHere": "从这里开始故事", - "testFromHere": "从这里开始测试" + "selectAllPassages": "选择所有片段" }, "zoomButtons": { "storyStructure": "仅显示故事结构", @@ -292,10 +306,14 @@ } }, "storyFormatList": { - "storyFormatExplanation": "故事格式用于控制游戏过程中的故事外观和行为。", + "noneVisible": "没有符合您筛选标准的故事格式。", + "show": "显示……", + "title": { + "all": "全部故事格式", + "current": "当前故事格式", + "user": "用户添加的故事格式" + }, "toolbar": { - "useAsProofingFormat": "用于校对故事", - "useAsDefaultFormat": "设为默认格式", "addStoryFormatButton": { "addPreview": "将添加 {{storyFormatName}} {{storyFormatVersion}}。", "alreadyAdded": "已添加 {{storyFormatName}} {{storyFormatVersion}}。", @@ -304,24 +322,29 @@ "prompt": "要添加故事格式,请在下方输入地址。" }, "disableFormatExtensions": "禁用编辑器扩展", - "enableFormatExtensions": "启用编辑器扩展" + "enableFormatExtensions": "启用编辑器扩展", + "useAsDefaultFormat": "设为默认格式", + "useAsProofingFormat": "用于校对故事" }, - "show": "显示……", - "noneVisible": "没有符合您筛选标准的故事格式。", - "title": { - "all": "全部故事格式", - "current": "当前故事格式", - "user": "用户添加的故事格式" - } + "storyFormatExplanation": "故事格式用于控制游戏过程中的故事外观和行为。" }, "storyList": { + "library": "库", "noStories": "在 Twine 中没有保存的故事。您可以创建一个新故事或从文件导入现有的故事。", + "taggedTitleCount": "1 个有标签的故事", + "taggedTitleCount_0": "不存在有标签的故事", + "taggedTitleCount_plural": "{{count}} 个有标签的故事", + "titleCount": "1 个故事", + "titleCount_0": "无故事", + "titleCount_plural": "{{count}} 个故事", "titleGeneric": "故事", "toolbar": { "archive": "档案", - "sort": "排序方式", - "sortByDate": "日期", - "sortByName": "名称", + "createStoryButton": { + "prompt": "您故事的名称是什么?您可以随后更改它。", + "emptyName": "请输入一个名称。", + "nameConflict": "其他故事已使用这一名称。" + }, "deleteStoryButton": { "warning": { "electron": "您确定要删除“{{storyName}}”吗?它将会被移入回收站。", @@ -330,50 +353,27 @@ }, "showAllStories": "显示所有故事", "showTags": "显示标签", + "sort": "排序方式", + "sortByDate": "日期", + "sortByName": "名称", "storyTags": "故事标签" - }, - "titleCount_plural": "{{count}} 个故事", - "library": "库", - "taggedTitleCount_0": "不存在有标签的故事", - "taggedTitleCount": "1 个有标签的故事", - "taggedTitleCount_plural": "{{count}} 个有标签的故事", - "titleCount": "1 个故事", - "titleCount_0": "无故事" + } }, "welcome": { + "autosave": "

您的 Documents 文件夹中现在有一个名为 Twine 的文件夹。 里面是一个故事文件夹,所有的作品都将被保存。 Twine 自动为你保存作品,所以你不必担心自己忘记保存。 您可以随时使用 Twine 上菜单的打开故事库项,来打开您的故事库文件夹。

由于 Twine 始终保存您的作品,因此在 Twine 打开时,故事库中的文件将被锁定而无法编辑。

如果您想打开从别人那里获得的故事,可以使用故事列表中的从文件导入链接来向故事库中导入文件。

", "autosaveTitle": "您的作品将自动保存。", + "browserStorage": "

这意味着你不需要创建一个账号来使用 Twine 2,并且你创建的所有内容都不会被存储在其他地方的服务器上,它只保存在你的浏览器中。

不过,要记住两件非常重要的事情。 由于您的作品仅保存在您的浏览器中,因此如果您清除了保存的数据,那么您将失去作品! 不好。 请记住经常使用存档按钮。 您还可以使用故事列表中每个故事的菜单将单个故事发布到文件。 档案和故事文件都可以重新导入到 Twine 中。

其次,任何可以使用此浏览器的人都可以查看并更改您的作品。所以,如果你家里有一位熊孩子角色,最好为自己做一个文件备份。

", + "browserStorageTitle": "您的作品仅保存在浏览器中", + "done": "

感谢您的阅读,希望使用 Twine 愉快!

", "doneTitle": "就是这样!", "gotoStoryList": "返回故事列表", + "greeting": "

Twine 是一个开源的工具,用于展示互动的非线性故事。 在开始之前,你应该了解一些事情。

", "greetingTitle": "(。・∀・)ノ゙嗨!", "tellMeMore": "告诉我更多", - "helpTitle": "新来的?", "help": "

如果你是第一次使用 Twine,那么欢迎你来使用 Twine!这个 Twine 指南书可以很好地教你如何使用 Twine。你之前从未用过 Twine 的话,这是个不错的开始。

", - "autosave": "

您的 Documents 文件夹中现在有一个名为 Twine 的文件夹。 里面是一个故事文件夹,所有的作品都将被保存。 Twine 自动为你保存作品,所以你不必担心自己忘记保存。 您可以随时使用 Twine 上菜单的打开故事库项,来打开您的故事库文件夹。

由于 Twine 始终保存您的作品,因此在 Twine 打开时,故事库中的文件将被锁定而无法编辑。

如果您想打开从别人那里获得的故事,可以使用故事列表中的从文件导入链接来向故事库中导入文件。

", - "browserStorage": "

这意味着你不需要创建一个账号来使用 Twine 2,并且你创建的所有内容都不会被存储在其他地方的服务器上,它只保存在你的浏览器中。

不过,要记住两件非常重要的事情。 由于您的作品仅保存在您的浏览器中,因此如果您清除了保存的数据,那么您将失去作品! 不好。 请记住经常使用存档按钮。 您还可以使用故事列表中每个故事的菜单将单个故事发布到文件。 档案和故事文件都可以重新导入到 Twine 中。

其次,任何可以使用此浏览器的人都可以查看并更改您的作品。所以,如果你家里有一位熊孩子角色,最好为自己做一个文件备份。

", - "greeting": "

Twine 是一个开源的工具,用于展示互动的非线性故事。 在开始之前,你应该了解一些事情。

", - "browserStorageTitle": "您的作品仅保存在浏览器中", - "done": "

感谢您的阅读,希望使用 Twine 愉快!

" + "helpTitle": "新来的?" } }, - "store": { - "passageDefaults": { - "name": "未命名片段" - }, - "storyDefaults": { - "name": "未命名故事" - }, - "storyFormatDefaults": { - "name": "未命名故事的格式" - }, - "errors": { - "cantPersistPrefs": "保存偏好设置失败({{error}})。", - "cantPersistStories": "保存故事失败({{error}})。", - "cantPersistStoryFormats": "保存故事格式失败({{error}})。", - "electronRemediation": "重启应用可能会解决问题。", - "webRemediation": "重新载入该页面可能会解决问题。" - }, - "archiveFilename": "{{timestamp}} Twine 档案.html" - }, "routeActions": { "app": { "aboutApp": "关于 Twine", @@ -382,10 +382,44 @@ "storyFormats": "故事格式" }, "build": { - "proof": "校对", - "test": "测试", + "exportAsTwee": "导出为Twee", "play": "运行", - "publishToFile": "发布到文件" + "proof": "校对", + "publishToFile": "发布到文件", + "test": "测试" + } + }, + "store": { + "archiveFilename": "{{timestamp}} Twine 档案.html", + "errors": { + "cantPersistPrefs": "保存偏好设置失败({{error}})。", + "cantPersistStories": "保存故事失败({{error}})。", + "cantPersistStoryFormats": "保存故事格式失败({{error}})。", + "electronRemediation": "重启应用可能会解决问题。", + "webRemediation": "重新载入该页面可能会解决问题。" + }, + "passageDefaults": { + "name": "未命名片段" + }, + "storyDefaults": { + "name": "未命名故事" + }, + "storyFormatDefaults": { + "name": "未命名故事的格式" } + }, + "undoChange": { + "addTag": "添加标签", + "changeTagColor": "更改标签颜色", + "newPassage": "新建片段", + "deletePassage": "删除片段", + "deletePassages": "删除片段", + "movePassage": "移动片段", + "movePassages": "移动片段", + "imortTag": "移除标签", + "renamePassage": "重命名片段", + "removeTag": "移除标签", + "renameTag": "重命名标签", + "replaceAllText": "替换所有" } } diff --git a/public/story-formats/harlowe-3.3.3/format.js b/public/story-formats/harlowe-3.3.3/format.js deleted file mode 100644 index b4b98b5e3..000000000 --- a/public/story-formats/harlowe-3.3.3/format.js +++ /dev/null @@ -1,4 +0,0 @@ -window.storyFormat({"name":"Harlowe","version":"3.3.3","author":"Leon Arnott","description":"The default story format for Twine 2, with numerous programming features and a rich passage editor. No HTML, JS or CSS experience required. Consult its documentation.","image":"icon.svg","url":"http://twinery.org/","license":"Zlib","proofing":false,"source":"\n\n\n\n\n{{STORY_NAME}}\n\n\n\n\n{{STORY_DATA}}\n\n\n\n\n","setup": function(){(function(){ -"use strict";function _createForOfIteratorHelper(a,t){var r,o="undefined"!=typeof Symbol&&a[Symbol.iterator]||a["@@iterator"];if(!o){if(Array.isArray(a)||(o=_unsupportedIterableToArray(a))||t&&a&&"number"==typeof a.length)return o&&(a=o),r=0,{s:t=function F(){},n:function n(){return r>=a.length?{done:!0}:{done:!1,value:a[r++]}},e:function e(a){throw a},f:t};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,c=!0,l=!1;return{s:function s(){o=o.call(a)},n:function n(){var e=o.next();return c=e.done,e},e:function e(a){l=!0,i=a},f:function f(){try{c||null==o.return||o.return()}finally{if(l)throw i}}}}function _toArray(e){return _arrayWithHoles(e)||_iterableToArray(e)||_unsupportedIterableToArray(e)||_nonIterableRest()}function _slicedToArray(e,a){return _arrayWithHoles(e)||_iterableToArrayLimit(e,a)||_unsupportedIterableToArray(e,a)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _iterableToArrayLimit(e,a){var t=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=t){var r,o,n=[],i=!0,s=!1;try{for(t=t.call(e);!(i=(r=t.next()).done)&&(n.push(r.value),!a||n.length!==a);i=!0);}catch(e){s=!0,o=e}finally{try{i||null==t.return||t.return()}finally{if(s)throw o}}return n}}function _arrayWithHoles(e){if(Array.isArray(e))return e}function _toConsumableArray(e){return _arrayWithoutHoles(e)||_iterableToArray(e)||_unsupportedIterableToArray(e)||_nonIterableSpread()}function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(e,a){if(e){if("string"==typeof e)return _arrayLikeToArray(e,a);var t=Object.prototype.toString.call(e).slice(8,-1);return"Map"===(t="Object"===t&&e.constructor?e.constructor.name:t)||"Set"===t?Array.from(e):"Arguments"===t||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)?_arrayLikeToArray(e,a):void 0}}function _iterableToArray(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}function _arrayWithoutHoles(e){if(Array.isArray(e))return _arrayLikeToArray(e)}function _arrayLikeToArray(e,a){(null==a||a>e.length)&&(a=e.length);for(var t=0,r=new Array(a);t=n.length)&&!g.isFront)continue}s=this.end)return null;if(this.children.length)for(var a=0;a=this.end)return[];var a=[];if(this.children.length)for(var t=0;t=this.end?null:this.children?this.children.reduce(function(e,a){return e||(t>=a.start&&t|<=+|=+><=+|<==+>)"+o+l,l=o+"(=+\\|+|\\|+=+|=+\\|+=+|\\|=+\\|)"+o+l,p={opener:"\\[\\[(?!\\[)",text:"("+function notChars(){return"[^"+Array.apply(0,arguments).map(escape).join("")+"]*"}("]")+")",rightSeparator:a("\\->","\\|"),leftSeparator:"<\\-",closer:"\\]\\]",legacySeparator:"\\|",legacyText:"("+a("[^\\|\\]]","\\]"+t("\\]"))+"+)"},g=c+"*"+c.replace("\\w","a-zA-Z")+c+"*",b="\\$("+g+")",y="_("+g+")",f="'s"+n+"("+g+")",w="("+g+")"+n+"of"+i+t("it\\b"),k="'s"+n,v=a("it","time","turns?","visits?","exits?","pos")+i,x="its"+n+"("+g+")",T="("+g+")"+n+"of"+n+"it"+i,C="of"+n+"it"+i,S={opener:"\\(",name:"("+a("\\$","_")+"?"+s+"+):"+t("\\/"),closer:"\\)"},A=a("=<","=>","[gl]te?\\b","n?eq\\b","isnot\\b","are\\b","x\\b","isa\\b","or"+n+"a"+i),N="[a-zA-Z][\\w\\-]*",_="(?:\"[^\"]*\"|'[^']*'|[^'\">])*?",O="\\|("+s+"+)(>|\\))",L="(<|\\()("+s+"+)\\|",P="((?:\\b\\d+(?:\\.\\d+)?|\\.\\d+)(?:[eE][+\\-]?\\d+)?)"+t("m?s")+i;p.main=p.opener+a(p.text+p.rightSeparator,p.text.replace("*","*?")+p.leftSeparator)+p.text,e={upperLetter:"[A-Z\\u00c0-\\u00de\\u0150\\u0170]",lowerLetter:"[a-z0-9_\\-\\u00df-\\u00ff\\u0151\\u0171]",anyLetter:s,anyLetterStrict:c,whitespace:n.replace("[","[\\n\\r"),escapedLine:"\\\\\\n\\\\?|\\n\\\\",br:"\\n(?!\\\\)",tag:"<\\/?"+N+_+">",scriptStyleTag:"<("+a("script","style","textarea")+")"+_+">[^]*?<\\/\\1>",scriptStyleTagOpener:"<",url:"("+a("https?","mailto","javascript","ftp","data")+":\\/\\/[^\\s<]+[^<.,:;\"')\\]\\s])",bullet:"\\*",hr:m,heading:"[ \\f\\t\\v\\u00a0\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]*(#{1,6})[ \\f\\t\\v\\u00a0\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]*",align:u,column:l,bulleted:h,numbered:d,verbatimOpener:"`+",hookAppendedFront:"\\["+t("=+"),hookPrependedFront:O+"\\["+t("=+"),hookFront:"\\["+t("=+"),hookBack:"\\]"+t(L),hookAppendedBack:"\\]"+L,unclosedHook:"\\[=+",unclosedHookPrepended:O+"\\[=+",unclosedCollapsed:"\\{=+",passageLink:p.main+p.closer,legacyLink:p.opener+p.legacyText+p.legacySeparator+p.legacyText+p.closer,simpleLink:p.opener+p.legacyText+p.closer,macroFront:S.opener+r(S.name),macroName:S.name,groupingFront:"\\("+t(S.name),twine1Macro:"<<[^>\\s]+\\s*(?:\\\\.|'(?:[^'\\\\]*\\\\.)*[^'\\\\]*'|\"(?:[^\"\\\\]*\\\\.)*[^\"\\\\]*\"|[^'\"\\\\>]|>(?!>))*>>",validPropertyName:g,property:f,belongingProperty:w,possessiveOperator:k,belongingOperator:"of\\b",itsOperator:"its\\b",belongingItOperator:C,variable:b,tempVariable:y,hookName:"\\?("+s+"+)\\b",cssTime:"(\\d+\\.?\\d*|\\d*\\.?\\d+)(m?s)\\b",colour:a(a("Red","Orange","Yellow","Lime","Green","Cyan","Aqua","Blue","Navy","Purple","Fuchsia","Magenta","White","Gray","Grey","Black","Transparent"),"#[\\dA-Fa-f]{3}(?:[\\dA-Fa-f]{3})?"),datatype:a("alnum","alphanumeric","any(?:case)?","array","bool(?:ean)?","changer","codehook","colou?r","const","command","dm","data"+a("map","type","set"),"ds","digit","gradient","empty","even","int"+t("o")+"(?:eger)?","lambda","lowercase","macro","linebreak","newline","num(?:ber)?","odd","str(?:ing)?","uppercase","whitespace")+i,number:P,boolean:a("true","false")+i,identifier:v,itsProperty:x,belongingItProperty:T,escapedStringChar:"\\\\[^\\n]",singleStringOpener:"'",doubleStringOpener:'"',singleStringCloser:"'",doubleStringCloser:'"',is:"is"+t(n+"not"+i,n+"an?"+i,n+"in"+i,n+"<",n+">")+i,isNot:"is"+n+"not"+t(n+a("an?","in")+i)+i,isA:"is"+n+"an?"+i,isNotA:"is"+n+"not"+n+"an?"+i,matches:"matches\\b",doesNotMatch:"does"+n+"not"+n+"match"+i,and:"and\\b",or:"or\\b",not:"not\\b",inequality:"((?:is(?:"+n+"not)?"+o+")*)("+a("<(?!=)","<=",">(?!=)",">=")+")",isIn:"is"+n+"in"+i,contains:"contains\\b",doesNotContain:"does"+n+"not"+n+"contain"+i,isNotIn:"is"+n+"not"+n+"in"+i,addition:escape("+")+t("="),subtraction:escape("-")+t("=","type"),multiplication:escape("*")+t("="),division:a("/","%")+t("="),spread:"\\.\\.\\."+t("\\."),to:a("to\\b","="),into:"into\\b",making:"making\\b",where:"where\\b",when:"when\\b",via:"via\\b",each:"each\\b",augmentedAssign:a("\\+","\\-","\\*","\\/","%")+"=",bind:"2?bind\\b",typeSignature:escape("-type")+i,incorrectOperator:A,PlainCompare:{comma:",",commentFront:"\x3c!--",commentBack:"--\x3e",strikeOpener:"~~",italicOpener:"//",boldOpener:"''",supOpener:"^^",strongFront:"**",strongBack:"**",emFront:"*",emBack:"*",collapsedFront:"{",collapsedBack:"}",groupingBack:")"}},"object"===("undefined"==typeof module?"undefined":_typeof(module))?module.exports=e:"function"==typeof define&&define.amd?define("patterns",[],function(){return e}):this&&this.loaded?(this.modules||(this.modules={}),this.modules.Patterns=e):this.Patterns=e}.call(eval("this")||("undefined"!=typeof global?global:window)),!function(){Object.assign=Object.assign||function polyfilledAssign(e){for(var a=1;a<");return~t?25===(a=Math.round(t/(e.length-2)*50))&&(a="center"):"<"===e[0]&&">"===e.slice(-1)?a="justify":-1")?a="right":-1":">=","=<":"<=",gte:">=",lte:"<=",gt:">",lt:"<",eq:"is",isnot:"is not",neq:"is not",isa:"is a",are:"is",x:"*","or a":"or"}[e[0].toLowerCase().replace(/\s+/g," ")];return{type:"error",message:"Please say "+(a?"'"+a+"'":"something else")+" instead of '"+e[0]+"'.",explanation:"In the interests of readability, I want certain operators to be in a specific form."}},cannotFollowText:!0}},["boolean","is","to","into","where","when","via","making","each","and","or","not","isNot","contains","doesNotContain","isIn","isA","isNotA","isNotIn","matches","doesNotMatch","bind"].reduce(function(e,a){return e[a]={fn:t,cannotFollowText:!0},e},{}),["comma","spread","typeSignature","addition","subtraction","multiplication","division"].reduce(function(e,a){return e[a]={fn:t},e},{}))),h=setupRules(o,{singleStringCloser:l.singleStringOpener,doubleStringCloser:l.doubleStringOpener,escapedStringChar:l.escapedStringChar}),d=(r.push.apply(r,_toConsumableArray(u(n)).concat(_toConsumableArray(u(c)),_toConsumableArray(u(s)))),a.push.apply(a,_toConsumableArray(u(c)).concat(_toConsumableArray(u(l)))),o.push.apply(o,_toConsumableArray(u(h))),p({},n,s,c,l,h));return u(d).forEach(function(e){m.PlainCompare[e]?(d[e].pattern=m.PlainCompare[e],d[e].plainCompare=!0):d[e].pattern=RegExp("^(?:"+m[e]+")","i")}),p(e.rules,d),(s=e.modes).start=s.markup=r,s.macro=a,s.string=o,e}(e).lex,Patterns:m})}"object"===("undefined"==typeof module?"undefined":_typeof(module))?(m=require("./patterns"),module.exports=exporter(require("./lexer"))):"function"==typeof define&&define.amd?define("markup",["lexer","patterns"],function(e,a){return m=a,exporter(e)}):this&&this.loaded&&this.modules?(m=this.modules.Patterns,this.modules.Markup=exporter(this.modules.Lexer)):(m=this.Patterns,this.Markup=exporter(this.Lexer))}.call(eval("this")||("undefined"!=typeof global?global:window)),!function(){var a=Math.round,e=function insensitiveName(e){return(e+"").toLowerCase().replace(/-|_/g,"")},t={"#e61919":"red","#e68019":"orange","#e5e619":"yellow","#80e619":"lime","#19e619":"green","#19e5e6":"cyan","#197fe6":"blue","#1919e6":"navy","#7f19e6":"purple","#e619e5":"magenta","#ffffff":"white","#000000":"black","#888888":"grey"},r=function fontIcon(e){var a=1')},o=function GCD(e,a){return e?a?a