From e017b6b6f41a370956108c0fd5fe73eb787d4521 Mon Sep 17 00:00:00 2001 From: Olga Bulat Date: Wed, 4 Dec 2024 08:18:22 +0300 Subject: [PATCH] Rewrite the i18n scripts (#5177) * Rewrite the i18n scripts - Convert CJS to MJS - Use flat keys and simplify json handling - Update ESLint rule to allow for dot in keys * Update casing for Glotpress and simplify ESLint rule * Deduplicate user-agent constant * Add Vue to TermCasing --- .codespell/ignore_lines.txt | 2 +- .vale/styles/Openverse/TermCasing.yml | 13 +- documentation/frontend/reference/i18n.md | 51 +- frontend/i18n/README.md | 10 + frontend/i18n/data/en.json5 | 2068 +++++++---------- frontend/i18n/locales/README.md | 5 - frontend/i18n/locales/scripts/README.md | 4 - frontend/i18n/locales/scripts/axios.js | 7 - .../i18n/locales/scripts/bulk-download.js | 86 - .../locales/scripts/create-wp-locale-list.js | 120 - .../scripts/get-translations-status.js | 48 - .../i18n/locales/scripts/get-translations.js | 54 - .../locales/scripts/get-validated-locales.js | 86 - .../locales/scripts/jed1x-json-to-json.js | 69 - frontend/i18n/locales/scripts/json-helpers.js | 64 - .../i18n/locales/scripts/json-pot-helpers.js | 163 -- frontend/i18n/locales/scripts/json-to-pot.js | 15 - .../i18n/locales/scripts/ngx-json-to-json.js | 47 - frontend/i18n/locales/scripts/read-i18n.js | 192 -- frontend/i18n/locales/scripts/types.d.ts | 9 - frontend/i18n/locales/scripts/utils.js | 145 -- frontend/i18n/scripts/generate-pot.mjs | 48 + frontend/i18n/scripts/metadata.mjs | 259 +++ frontend/i18n/scripts/paths.mjs | 19 + .../po/parse-vue-files.mjs} | 83 +- frontend/i18n/scripts/po/po-helpers.mjs | 177 ++ frontend/i18n/scripts/setup.mjs | 115 + frontend/i18n/scripts/translations.mjs | 231 ++ frontend/i18n/scripts/types.d.ts | 48 + frontend/i18n/scripts/utils.mjs | 40 + frontend/nuxt.config.ts | 2 +- frontend/package.json | 25 +- .../{user-agent.js => user-agent.mjs} | 6 +- frontend/shared/utils/attribution-html.ts | 43 +- frontend/src/data/api-service.ts | 13 +- frontend/test/README.md | 2 +- frontend/test/locales/ar.json | 1500 +++++------- frontend/test/locales/es.json | 1500 +++++------- frontend/test/locales/ru.json | 472 ++-- frontend/test/locales/valid-locales.json | 3 - frontend/test/playwright/e2e/filters.spec.ts | 7 +- .../test/playwright/e2e/report-media.spec.ts | 2 +- frontend/test/playwright/utils/i18n.ts | 39 +- frontend/test/playwright/utils/navigation.ts | 2 +- .../unit/specs/utils/attribution-html.spec.ts | 5 +- justfile | 2 +- packages/js/eslint-plugin/src/configs/vue.ts | 23 +- packages/js/eslint-plugin/src/index.ts | 1 + packages/js/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/key-name-casing.ts | 110 + .../test/rules/key-name-casing.spec.ts | 137 ++ pnpm-lock.yaml | 3 + utilities/generate_test_locales/__main__.py | 2 +- 53 files changed, 3446 insertions(+), 4733 deletions(-) create mode 100644 frontend/i18n/README.md delete mode 100644 frontend/i18n/locales/README.md delete mode 100644 frontend/i18n/locales/scripts/README.md delete mode 100644 frontend/i18n/locales/scripts/axios.js delete mode 100644 frontend/i18n/locales/scripts/bulk-download.js delete mode 100644 frontend/i18n/locales/scripts/create-wp-locale-list.js delete mode 100644 frontend/i18n/locales/scripts/get-translations-status.js delete mode 100644 frontend/i18n/locales/scripts/get-translations.js delete mode 100644 frontend/i18n/locales/scripts/get-validated-locales.js delete mode 100644 frontend/i18n/locales/scripts/jed1x-json-to-json.js delete mode 100644 frontend/i18n/locales/scripts/json-helpers.js delete mode 100644 frontend/i18n/locales/scripts/json-pot-helpers.js delete mode 100644 frontend/i18n/locales/scripts/json-to-pot.js delete mode 100644 frontend/i18n/locales/scripts/ngx-json-to-json.js delete mode 100644 frontend/i18n/locales/scripts/read-i18n.js delete mode 100644 frontend/i18n/locales/scripts/types.d.ts delete mode 100644 frontend/i18n/locales/scripts/utils.js create mode 100644 frontend/i18n/scripts/generate-pot.mjs create mode 100644 frontend/i18n/scripts/metadata.mjs create mode 100644 frontend/i18n/scripts/paths.mjs rename frontend/i18n/{locales/scripts/parse-vue-files.js => scripts/po/parse-vue-files.mjs} (58%) create mode 100644 frontend/i18n/scripts/po/po-helpers.mjs create mode 100644 frontend/i18n/scripts/setup.mjs create mode 100644 frontend/i18n/scripts/translations.mjs create mode 100644 frontend/i18n/scripts/types.d.ts create mode 100644 frontend/i18n/scripts/utils.mjs rename frontend/shared/constants/{user-agent.js => user-agent.mjs} (64%) create mode 100644 packages/js/eslint-plugin/src/rules/key-name-casing.ts create mode 100644 packages/js/eslint-plugin/test/rules/key-name-casing.spec.ts diff --git a/.codespell/ignore_lines.txt b/.codespell/ignore_lines.txt index 3b339812842..caeeb4e8e51 100644 --- a/.codespell/ignore_lines.txt +++ b/.codespell/ignore_lines.txt @@ -8,7 +8,7 @@ ;; frontend/i18n/data/en.json5 ;; Prettier insists we escape a single quote rather than the double quotes and codespell ;; does not understand the escaped `\'t` as "couldn't". It instead just sees "couldn". - heading: 'We couldn\'t find anything for "{query}".', + "noResults.heading": 'We couldn\'t find anything for "{query}".', ;; catalog/tests/dags/providers/provider_api_scripts/test_wikimedia_commons.py ;; "Titel" matches "title", but the phrase is in Dutch, not English, so "titel" diff --git a/.vale/styles/Openverse/TermCasing.yml b/.vale/styles/Openverse/TermCasing.yml index db9fc5e17ac..1f00d9559bf 100644 --- a/.vale/styles/Openverse/TermCasing.yml +++ b/.vale/styles/Openverse/TermCasing.yml @@ -12,12 +12,13 @@ swap: # [^/\.] prevents matching things that look like URLs, file paths, or GitHub team mentions # For example: @WordPress/openverse-maintainers '[^/\.]openverse[^/\.]': Openverse - # OpenVerse should never be used, except as an example of something that is always wrong, - # in which case we'll tell Vale to ignore that line. - "OpenVerse": Openverse '[^/\.]wordpress[^/\.]': WordPress - # Wordpress is the same as OpenVerse - "Wordpress": WordPress '[^/\.]github[^/\.]': GitHub - # Github is the same as Wordpress and OpenVerse + '[^/\.]vue[^/\.]': Vue + # OpenVerse, Wordpress, Github and Glotpress should never be used, except as an example of + # something that is always wrong, in which case we'll tell Vale to ignore that line. + "OpenVerse": Openverse + "Wordpress": WordPress "Github": GitHub + "Glotpress": GlotPress + "vue": Vue diff --git a/documentation/frontend/reference/i18n.md b/documentation/frontend/reference/i18n.md index 6cbe02a096b..7481444f32e 100644 --- a/documentation/frontend/reference/i18n.md +++ b/documentation/frontend/reference/i18n.md @@ -9,7 +9,7 @@ WordPress uses GlotPress for managing translations, which is built on top of the Nuxt (and most JS-based i18n libraries) use [JSON](https://kazupon.github.io/vue-i18n/guide/formatting.html) for managing translations. This disconnect means that Openverse translations must convert -from JSON to POT and back again. Hence there is quite a bit of scaffolding +from JSON to POT and back again. Hence, there is quite a bit of scaffolding involved. ## Upload pipeline @@ -25,7 +25,7 @@ frontend application and provided to GlotPress for translation. - Upload this [POT file](https://github.com/WordPress/openverse/blob/translations/openverse.pot) - to a fixed URL. Currently the file is hosted in the `translations` branch of + to a fixed URL. Currently, the file is hosted in the `translations` branch of the [WordPress/openverse](https://github.com/WordPress/openverse) repo. - GlotPress presents the strings, fuzzy translations and other helpful context @@ -34,42 +34,39 @@ frontend application and provided to GlotPress for translation. ## Download pipeline This pipeline deals with how translations are retrieved from GlotPress, -processed and loaded into Nuxt via the Nuxt i18n module. +processed and loaded into Nuxt via the Nuxt i18n module. The main entry point, +`i18n/scripts/setup.mjs`, orchestrates the entire process based on the command +line arguments. There are three main modes of operation: -### Steps - -- Parse and extract the list of all locales from GlotPress's PHP source code. - Then narrow down the list to locales available in the WP GlotPress instance - and populate their coverage percentage from the - [GlotPress stats](https://translate.wordpress.org/projects/meta/openverse/). - - The output is written to `wp-locales.json`. +- for production, it can download translations from GlotPress, parse the + locales, and update the Vue i18n plugin configuration. +- for local development, it can convert `en.json5` to JSON format used by the + Nuxt app, and save the empty `valid-locales.json` file to be used by the Nuxt + app. +- for testing, it can copy the test locale metadata, and test translations to + the main `i18n/locales` folder. - **Script:** `i18n:create-locales-list` +### Production Steps -- Download all translations from GlotPress as JED 1.x JSON files. The flattened - JED 1.x (derived from the flattened POT files) files are converted back into - the nested JSON as expected by Nuxt i18n. +- Download all translations from GlotPress as NGX JSON files - flat json files. This script downloads all available translations in bulk as a ZIP file and then extracts JSON files from the ZIP file. This prevents excessive calls to GlotPress, which can be throttled and cause some locales to be missed. - **Script:** `i18n:get-translations` + **Script:** `i18n/scripts/translations.mjs` -- Separate the locales into three groups based on the JSON files emitted by - `i18n:get-translations`. +- Parse and extract the list of all locales from GlotPress's PHP source code. + Then narrow down the list to locales available in the WP GlotPress instance + and calculate their coverage percentage from the number of keys in the + translation and the number of keys in the main `en.json5` file. After that, + separate the locale metadata into two groups based on the JSON files emitted + by the previous step. - **translated:** JSON file is present with mappings, written to `valid-locales.json`. - - **untranslated:** JSON file is present but empty, written to both - `valid-locales.json` and `untranslated-locales.json`. - - **invalid:** JSON file is not present, written to `invalid-locales.json`. - - **Script:** `i18n:update-locales` - -- Pass the list of valid locales (along with extra fields) into the Nuxt i18n - module. This is configured in the Nuxt configuration file, `nuxt.config.ts`. + - (only if --verbose flag is on) **untranslated:** JSON file is present but + empty, written to `untranslated-locales.json`. - Pass the fallback locale mappings to the underlying Vue i18n plugin. This is configured in the plugin configuration file, @@ -78,7 +75,7 @@ processed and loaded into Nuxt via the Nuxt i18n module. ## Test locales Three locales are maintained in the code base (as opposed to downloaded from -Glotpress) for use in testing, alongside the default English locale. These +GlotPress) for use in testing, alongside the default English locale. These locales are **not** meant to be representative of actual final translations. They are merely used to approximate length and language features that help identify potential layout issues in the application. diff --git a/frontend/i18n/README.md b/frontend/i18n/README.md new file mode 100644 index 00000000000..0b7916203a7 --- /dev/null +++ b/frontend/i18n/README.md @@ -0,0 +1,10 @@ +# Locales + +The primary internationalisation file is [`data/en.json5`](../data/en.json5). +All `.json` files present in the [`locales`](./locales) directory are +re-generated when updating translations, so they should not be modified. + +# Locale scripts + +Locale scripts should be run in the root of the repository using their +respective pnpm commands, in the `i18n` namespace. diff --git a/frontend/i18n/data/en.json5 b/frontend/i18n/data/en.json5 index a42ccaa7a10..466196d91db 100644 --- a/frontend/i18n/data/en.json5 +++ b/frontend/i18n/data/en.json5 @@ -1,1238 +1,844 @@ { - "404": { - title: "The content you’re looking for seems to have disappeared.", - main: "Go to {link} or search for something similar from the field below.", - }, - hero: { - subtitle: "Explore more than 800 million creative works", - description: "An extensive library of free stock photos, images, and audio, available for free use.", - search: { - placeholder: "Search for content", - }, - disclaimer: { - content: "All {openverse} content is under a {license} or is in the public domain.", - /** - Interpolated into hero.disclaimer.content: - _{license}_ part of "All Openverse content is under a {license} or is in the public domain." - */ - license: "Creative Commons license", - }, - }, - notification: { - translation: { - text: "The translation for {locale} locale is incomplete. Help us get to 100 percent by {link}.", - /** - Interpolated into notification.translation.text: - _{link}_ part of "The translation for English locale is incomplete. Help us get to 100 percent by _{link}_." + "404.title": "The content you’re looking for seems to have disappeared.", + "404.main": "Go to {link} or search for something similar from the field below.", + "hero.subtitle": "Explore more than 800 million creative works", + "hero.description": "An extensive library of free stock photos, images, and audio, available for free use.", + "hero.search.placeholder": "Search for content", + "hero.disclaimer.content": "All {openverse} content is under a {license} or is in the public domain.", + /** + Interpolated into hero.disclaimer.content: + _{license}_ part of "All Openverse content is under a {license} or is in the public domain." */ - link: "contributing a translation", - close: "Close the translation contribution help request banner", - }, - analytics: { - text: "Openverse uses analytics to improve the quality of our service. Visit {link} to learn more or opt out.", - /** - Interpolated into notification.analytics.text: - _{link}_ part of "Visit _{link}_ to learn more or opt out." - */ - link: "the privacy page", - close: "Close the analytics notice banner.", - }, - darkMode: { - text: "Dark mode is now available.", - }, - new: "New", - more: "See more", - }, - header: { - homeLink: "{openverse} Home", - placeholder: "Search all content", - aria: { - primary: "primary", - menu: "menu", - search: "search", - srSearch: "search button", - }, - aboutTab: "About", - resourcesTab: "Resources", - loading: "Loading...", - filterButton: { - simple: "Filters", - /** Used as the accessible label for the button or tab that opens the filters sidebar. Count refers the number of filters the user has enabled, not the number of available filters */ - withCount: "Filters ({count})", - }, - seeResults: "See results", - backButton: "Go back", - contentSettingsButton: { - simple: "Menu", - withCount: "Menu. {count} filter applied|Menu. {count} filters applied", - }, - }, - navigation: { - about: "About", - licenses: "Licenses", - getInvolved: "Get involved", - api: "API", - terms: "Terms", - privacy: "Privacy", - feedback: "Feedback", - sources: "Sources", - externalSources: "External sources", - searchHelp: "Search help", - }, - about: { - title: "About {openverse}", - description: { - content: "{openverse} is a tool that allows openly licensed and public domain works to be discovered and used by everyone.", - }, - collection: { - content: { - a: "{openverse} searches across more than 800 million images and audio tracks from open APIs and the {commonCrawl} dataset.", - b: "We aggregate works from multiple public repositories, and facilitate reuse through features like one-click attribution.", - }, - }, - planning: { - content: { - /** about.planning.content.a-c are parts of a single statement */ - a: "Currently {openverse} only searches images and audio tracks, with search for video provided through External Sources.", - /** about.planning.content.a-c are parts of a single statement */ - b: "We plan to add additional media types such as open texts and 3D models, with the ultimate goal of providing access to the estimated 2.5 billion CC licensed and public domain works on the web.", - /** - * about.planning.content.a-c are parts of a single statement. - * "repository", "community" and "working" are interpolated from about.planning.frontend, - * about.planning.repository, about.planning.catalog, about.planning.community, and about.planning.working, respectively. - */ - c: "All of our code is open source and can be accessed at the {repository}. We {community}. You can see what {working}.", - }, - /** - * Interpolated into about.planning.content.c: - * _{repository}_ part of "All of our code is open source and can be accessed at the _{repository}_. - */ - repository: "{openverse} {github} repository", - /** - * Interpolated into about.planning.content.c: - * _{community}_ part of "We {community}. You can see what we’re currently working on." - */ - community: "welcome community contribution", - /** - * Interpolated into about.planning.content.c: - * _{working}_ part of "You can see what _{working}_." - */ - working: "we’re currently working on", - }, - transfer: { - content: { - /** Part of a single statement with about.transfer.content.b and about.transfer.content.c */ - a: "{openverse} is the successor to CC Search which was launched by Creative Commons in 2019, after its migration to WordPress in 2021.", - /** Part of a single statement with about.transfer.content.a and about.transfer.content.c */ - b: "You can read more about this transition in the official announcements from {creativeCommons} and {wordpress}.", - /** Part of a single statement with about.transfer.content.a and about.transfer.content.b */ - c: "We remain committed to our goal of tackling discoverability and accessibility of open access media.", - }, - }, - declaration: { - content: { - /** about.declaration.content.a-b are parts of a single statement */ - a: "{openverse} does not verify licensing information for individual works, or whether the generated attribution is accurate or complete.", - /** about.declaration.content.a-b are parts of a single statement */ - b: "Please independently verify the licensing status and attribution information before reusing the content. For more details, read the {terms}.", - }, - /** - * Interpolated into about.declaration.content.b: - * _{terms}_ part of "For more details, read the _{terms}_." - */ - terms: "{openverse} Terms of Use", - }, - }, - sources: { - title: "Sources", - detail: "Clicking on a {singleName} allows you to browse and filter items within that source.", - /** - * Interpolated into sources.detail: - * _{singleName}_ part of "Clicking on a _{singleName}_ allows you to browse and filter items within that source." - */ - singleName: "Source", - providers: { - source: "Source", - domain: "Domain", - item: "Total items", - }, - ccContent: { - where: "Where does the content on {openverse} come from?", - content: "There is openly licensed content hosted on millions of domains across the breadth of the internet. Our team systematically identifies providers hosting CC-licensed content. If it’s a good fit, we index that content and make it discoverable through {openverse}.", - provider: { - /** sources.ccContent.provider.a-b are parts of a single statement */ - a: "Some providers have multiple different groupings of content within them. {flickr} has sources ranging from NASA to personal photography. The {smithsonian} comprises a dozen diverse collections.", - /** sources.ccContent.provider.a-b are parts of a single statement */ - b: "Wikimedia Commons runs the gamut in terms of content, and is used by several galleries, libraries, archives, and museums highlighting some or all of their digitized collections.", - }, - europeana: "{openverse} is especially grateful for the work of {link}, an organization that works to digitize and make discoverable cultural heritage works across Europe. {openverse} is able to index hundreds of valuable sources through a single integration with the {linkApi}.", - }, - newContent: { - next: "How do we decide what sources to add next?", - integrate: "We have a never ending list of possible sources to research prior to integration. We ask ourselves questions like:", - impact: " What is the impact or importance of this source to our users? If it exists within a provider like Wikimedia Commons, is it valuable for our users to be able to filter by this source directly?", - reuse: "Is licensing and attribution information clearly displayed to enable confident reuse?", - totalItems: "How many new total items or new types of items can we bring to our users through this integration? Some sources are direct integrations, while others may be a source within a source.", - }, - suggestions: "We appreciate suggestions for new sources from our community of users.", - issueButton: "Suggest a new source", - aria: { - table: "sources table", - }, - heading: { - image: "Image Sources", - audio: "Audio Sources", - }, - }, - externalSourcesPage: { - title: "External Sources", - intro: "{openverse} is built on top of a catalog that indexes CC-licensed and public domain content from selected sources. Learn more about our {link}.", - link: "sources here", - license: { - /** externalSourcesPage.license.a-c are parts of a single statement */ - a: "However, there are many sources of CC-licensed and public domain media that we are not yet able to include in {openverse} search.", - /** externalSourcesPage.license.a-c are parts of a single statement */ - b: "This might be because they do not offer a public API, or that our contributors have not yet had time to integrate them into {openverse}.", - /** externalSourcesPage.license.a-c are parts of a single statement */ - c: "These are valued sources and we want to make sure that you are able to find the best openly licensed materials possible, regardless of where they are located.", - }, - new: { - title: "Can I suggest new external sources?", - content: "Yes, please! Create an {issue} in our GitHub repository or send us an {email} and tell us about the new sources you’d like to see included.", - /** - * Interpolated into externalSourcesPage.new.content: - * _{issue}_ part of "Create an _{issue}_ in our GitHub repository or send us an email and tell us about the new sources you’d like to see included." - */ - issue: "issue", - /** - * Interpolated into externalSourcesPage.new.content: - * _{email} part of "Create an issue in our GitHub repository or send us an {email} and tell us about the new sources you’d like to see included." - */ - email: "email", - }, - why: { - title: "Why did you build this?", - content: "For many years, Creative Commons has offered its users a dedicated search portal for searching platforms that have CC licensing filters built in. In fact, this is still maintained at {old}.", - new: { - /** externalSourcesPage.why.new.a-c are parts of a single statement */ - a: 'For users of the legacy CC Meta Search site, the "External Sources" feature on {openverse} will look familiar.', - /** externalSourcesPage.why.new.a-c are parts of a single statement */ - b: "The goal was to ensure that the functionality is not lost, but is updated and embedded within our new search engine for openly licensed content.", - /** externalSourcesPage.why.new.a-c are parts of a single statement */ - c: 'Additionally, the "External Sources" feature builds on this functionality, allowing us to quickly add new external sources as we discover them, and support new content types in the future.', - }, - ariaLabel: "feedback", - feedbackSuggestions: "We hope you enjoy, and if you have suggestions for improvement, leave us {feedback}.", - /** - * Interpolated into externalSourcesPage.why.feedbackSuggestions: - * _{feedback}_ part of "We hope you enjoy, and if you have suggestions for improvement, leave us _{feedback}_" - */ - feedbackLink: "feedback", - }, - relationships: { - /** externalSourcesPage.relationships.a-b are parts of a single statement */ - a: "This functionality also allows us to start conversations and build relationships with sources that may like to be included in {openverse} in the future.", - /** externalSourcesPage.relationships.a-b are parts of a single statement */ - b: "Finally, we can also offer external sources of media types we do not include in {openverse} yet, but plan to.", - }, - explanation: "You can find links to external sources at the bottom of every {openverse} search results page; on pages for searches which return no results; and on pages for media types we do not yet support but intend to.", - }, - privacy: { - title: "Privacy", - intro: { - content: "The {openverse} project seeks to make the privacy and safety of our users a priority. {openverse} adheres to the {link}. Please see that document for a full description of how {openverse} uses and protects any information that you give us.", - /** - * Interpolated into privacy.intro.content: - * _{link}_ part of "Openverse adheres to the _{link}_." */ - link: "privacy policy of all WordPress.org websites", - }, - cookies: { - title: "Cookies", - content: { - /** privacy.cookies.content.a-b are parts of a single statement */ - a: "{openverse} uses cookies to store information about visitor's preferences and information about their web browsers. We use this information to improve the user experience of the site.", - /** privacy.cookies.content.a-b are parts of a single statement */ - b: 'These are considered "Necessary" or "Strictly necessary cookies". You may disable these by changing your browser settings, but this may affect how {openverse} functions.', - }, - }, - contact: { - title: "Contact Us", - content: "Any questions about {openverse} and privacy can be sent to {email}, shared as a {issue}, or discussed with our community in the #openverse channel of the {chat}.", - /** - * Interpolated into privacy.contact.content: - * _{issue}_ part of "Any questions about Openverse and privacy can be sent to openverse@wordpress.org, shared as a _{issue}_, or discussed with our community in the #openverse channel of the Making WordPress Chat." - */ - issue: "GitHub issue", - /** - * Interpolated into privacy.contact.content: - * _{chat}_ part of "Any questions about Openverse and privacy can be sent to openverse@wordpress.org, shared as a Github issue, or discussed with our community in the #openverse channel of the _{chat}_." - */ - chat: "Making WordPress Chat", - }, - }, - searchGuide: { - title: "{openverse} Syntax Guide", - intro: "When you search, you can enter special symbols or words to your search term to make your search results more precise.", - exact: { - title: "Search for an exact match", - ariaLabel: "quote unquote Claude Monet", - claudeMonet: '"Claude Monet"', - content: "To search for an exact word or phrase, put it inside quotes. For example, {link}.", - }, - negate: { - title: "Excluding terms", - /** - * Interpolated into searchGuide.negate.content: - * _{operator}_ part of 'To exclude a term from your results, put the {operator} in front of it.' - */ - operatorName: "minus sign", - ariaLabel: "dog minus pug", - example: "dog -pug", - content: 'To exclude a term from your results, put the {operator} in front of it. Example: {link}{br} This will search for media related to "dog" but won\'t include results related to "pug".', - }, - }, - feedback: { - title: "Feedback", - intro: "Thank you for using {openverse}! We welcome your ideas for improving the tool below. To provide regular feedback, join the {slack} channel in the {makingWordpress} Slack workspace.", - improve: "Help us Improve", - report: "Report a Bug", - loading: "Loading...", - aria: { - improve: "help us improve form", - report: "report a bug form", - }, - }, + "hero.disclaimer.license": "Creative Commons license", + "notification.translation.text": "The translation for {locale} locale is incomplete. Help us get to 100 percent by {link}.", + /** +Interpolated into notification.translation.text: +_{link}_ part of "The translation for English locale is incomplete. Help us get to 100 percent by _{link}_." +*/ + "notification.translation.link": "contributing a translation", + "notification.translation.close": "Close the translation contribution help request banner", + "notification.analytics.text": "Openverse uses analytics to improve the quality of our service. Visit {link} to learn more or opt out.", + /** + Interpolated into notification.analytics.text: + _{link}_ part of "Visit _{link}_ to learn more or opt out." + */ + "notification.analytics.link": "the privacy page", + "notification.analytics.close": "Close the analytics notice banner.", + "notification.darkMode.text": "Dark mode is now available.", + "notification.new": "New", + "notification.more": "See more", + "header.homeLink": "{openverse} Home", + "header.placeholder": "Search all content", + "header.aria.primary": "primary", + "header.aria.menu": "menu", + "header.aria.search": "search", + "header.aria.srSearch": "search button", + "header.aboutTab": "About", + "header.resourcesTab": "Resources", + "header.loading": "Loading...", + "header.filterButton.simple": "Filters", + /** Used as the accessible label for the button or tab that opens the filters sidebar. Count refers the number of filters the user has enabled, not the number of available filters */ + "header.filterButton.withCount": "Filters ({count})", + "header.seeResults": "See results", + "header.backButton": "Go back", + "header.contentSettingsButton.simple": "Menu", + "header.contentSettingsButton.withCount": "Menu. {count} filter applied|Menu. {count} filters applied", + "navigation.about": "About", + "navigation.licenses": "Licenses", + "navigation.getInvolved": "Get involved", + "navigation.api": "API", + "navigation.terms": "Terms", + "navigation.privacy": "Privacy", + "navigation.feedback": "Feedback", + "navigation.sources": "Sources", + "navigation.externalSources": "External sources", + "navigation.searchHelp": "Search help", + "about.title": "About {openverse}", + "about.description.content": "{openverse} is a tool that allows openly licensed and public domain works to be discovered and used by everyone.", + "about.collection.content.a": "{openverse} searches across more than 800 million images and audio tracks from open APIs and the {commonCrawl} dataset.", + "about.collection.content.b": "We aggregate works from multiple public repositories, and facilitate reuse through features like one-click attribution.", + /** about.planning.content.a-c are parts of a single statement */ + "about.planning.content.a": "Currently {openverse} only searches images and audio tracks, with search for video provided through External Sources.", + /** about.planning.content.a-c are parts of a single statement */ + "about.planning.content.b": "We plan to add additional media types such as open texts and 3D models, with the ultimate goal of providing access to the estimated 2.5 billion CC licensed and public domain works on the web.", + /** + * about.planning.content.a-c are parts of a single statement. + * "repository", "community" and "working" are interpolated from about.planning.frontend, + * about.planning.repository, about.planning.catalog, about.planning.community, and about.planning.working, respectively. + */ + "about.planning.content.c": "All of our code is open source and can be accessed at the {repository}. We {community}. You can see what {working}.", + /** + * Interpolated into about.planning.content.c: + * _{repository}_ part of "All of our code is open source and can be accessed at the _{repository}_". + */ + "about.planning.repository": "{openverse} {github} repository", + /** + * Interpolated into about.planning.content.c: + * _{community}_ part of "We {community}. You can see what we’re currently working on." + */ + "about.planning.community": "welcome community contribution", + /** + * Interpolated into about.planning.content.c: + * _{working}_ part of "You can see what _{working}_." + */ + "about.planning.working": "we’re currently working on", + /** Part of a single statement with about.transfer.content.b and about.transfer.content.c */ + "about.transfer.content.a": "{openverse} is the successor to CC Search which was launched by Creative Commons in 2019, after its migration to WordPress in 2021.", + /** Part of a single statement with about.transfer.content.a and about.transfer.content.c */ "about.transfer.content.b": "You can read more about this transition in the official announcements from {creativeCommons} and {wordpress}.", + /** Part of a single statement with about.transfer.content.a and about.transfer.content.b */ "about.transfer.content.c": "We remain committed to our goal of tackling discoverability and accessibility of open access media.", + /** about.declaration.content.a-b are parts of a single statement */ + "about.declaration.content.a": "{openverse} does not verify licensing information for individual works, or whether the generated attribution is accurate or complete.", + /** about.declaration.content.a-b are parts of a single statement */ + "about.declaration.content.b": "Please independently verify the licensing status and attribution information before reusing the content. For more details, read the {terms}.", + /** + * Interpolated into about.declaration.content.b: + * _{terms}_ part of "For more details, read the _{terms}_." + */ + "about.declaration.terms": "{openverse} Terms of Use", + "sources.title": "Sources", + "sources.detail": "Clicking on a {singleName} allows you to browse and filter items within that source.", + /** + * Interpolated into sources.detail: + * _{singleName}_ part of "Clicking on a _{singleName}_ allows you to browse and filter items within that source." + */ + "sources.singleName": "Source", + "sources.providers.source": "Source", + "sources.providers.domain": "Domain", + "sources.providers.item": "Total items", + "sources.ccContent.where": "Where does the content on {openverse} come from?", + "sources.ccContent.content": "There is openly licensed content hosted on millions of domains across the breadth of the internet. Our team systematically identifies providers hosting CC-licensed content. If it’s a good fit, we index that content and make it discoverable through {openverse}.", + /** sources.ccContent.provider.a-b are parts of a single statement */ + "sources.ccContent.provider.a": "Some providers have multiple different groupings of content within them. {flickr} has sources ranging from NASA to personal photography. The {smithsonian} comprises a dozen diverse collections.", + /** sources.ccContent.provider.a-b are parts of a single statement */ + "sources.ccContent.provider.b": "Wikimedia Commons runs the gamut in terms of content, and is used by several galleries, libraries, archives, and museums highlighting some or all of their digitized collections.", + "sources.ccContent.europeana": "{openverse} is especially grateful for the work of {link}, an organization that works to digitize and make discoverable cultural heritage works across Europe. {openverse} is able to index hundreds of valuable sources through a single integration with the {linkApi}.", + "sources.newContent.next": "How do we decide what sources to add next?", + "sources.newContent.integrate": "We have a never ending list of possible sources to research prior to integration. We ask ourselves questions like:", + "sources.newContent.impact": " What is the impact or importance of this source to our users? If it exists within a provider like Wikimedia Commons, is it valuable for our users to be able to filter by this source directly?", + "sources.newContent.reuse": "Is licensing and attribution information clearly displayed to enable confident reuse?", + "sources.newContent.totalItems": "How many new total items or new types of items can we bring to our users through this integration? Some sources are direct integrations, while others may be a source within a source.", + "sources.suggestions": "We appreciate suggestions for new sources from our community of users.", + "sources.issueButton": "Suggest a new source", + "sources.aria.table": "sources table", + "sources.heading.image": "Image Sources", + "sources.heading.audio": "Audio Sources", + "externalSourcesPage.title": "External Sources", + "externalSourcesPage.intro": "{openverse} is built on top of a catalog that indexes CC-licensed and public domain content from selected sources. Learn more about our {link}.", + "externalSourcesPage.link": "sources here", + /** externalSourcesPage.license.a-c are parts of a single statement */ + "externalSourcesPage.license.a": "However, there are many sources of CC-licensed and public domain media that we are not yet able to include in {openverse} search.", + /** externalSourcesPage.license.a-c are parts of a single statement */ + "externalSourcesPage.license.b": "This might be because they do not offer a public API, or that our contributors have not yet had time to integrate them into {openverse}.", + /** externalSourcesPage.license.a-c are parts of a single statement */ + "externalSourcesPage.license.c": "These are valued sources and we want to make sure that you are able to find the best openly licensed materials possible, regardless of where they are located.", + "externalSourcesPage.new.title": "Can I suggest new external sources?", + "externalSourcesPage.new.content": "Yes, please! Create an {issue} in our GitHub repository or send us an {email} and tell us about the new sources you’d like to see included.", + /** + * Interpolated into externalSourcesPage.new.content: + * _{issue}_ part of "Create an _{issue}_ in our GitHub repository or send us an email and tell us about the new sources you’d like to see included." + */ + "externalSourcesPage.new.issue": "issue", + /** + * Interpolated into externalSourcesPage.new.content: + * _{email} part of "Create an issue in our GitHub repository or send us an {email} and tell us about the new sources you’d like to see included." + */ + "externalSourcesPage.new.email": "email", + "externalSourcesPage.why.title": "Why did you build this?", + "externalSourcesPage.why.content": "For many years, Creative Commons has offered its users a dedicated search portal for searching platforms that have CC licensing filters built in. In fact, this is still maintained at {old}.", + /** externalSourcesPage.why.new.a-c are parts of a single statement */ + "externalSourcesPage.why.new.a": 'For users of the legacy CC Meta Search site, the "External Sources" feature on {openverse} will look familiar.', + /** externalSourcesPage.why.new.a-c are parts of a single statement */ + "externalSourcesPage.why.new.b": "The goal was to ensure that the functionality is not lost, but is updated and embedded within our new search engine for openly licensed content.", + /** externalSourcesPage.why.new.a-c are parts of a single statement */ + "externalSourcesPage.why.new.c": 'Additionally, the "External Sources" feature builds on this functionality, allowing us to quickly add new external sources as we discover them, and support new content types in the future.', + "externalSourcesPage.why.ariaLabel": "feedback", + "externalSourcesPage.why.feedbackSuggestions": "We hope you enjoy, and if you have suggestions for improvement, leave us {feedback}.", + /** + * Interpolated into externalSourcesPage.why.feedbackSuggestions: + * _{feedback}_ part of "We hope you enjoy, and if you have suggestions for improvement, leave us _{feedback}_" + */ + "externalSourcesPage.why.feedbackLink": "feedback", + /** externalSourcesPage.relationships.a-b are parts of a single statement */ + "externalSourcesPage.relationships.a": "This functionality also allows us to start conversations and build relationships with sources that may like to be included in {openverse} in the future.", + /** externalSourcesPage.relationships.a-b are parts of a single statement */ + "externalSourcesPage.relationships.b": "Finally, we can also offer external sources of media types we do not include in {openverse} yet, but plan to.", + "externalSourcesPage.explanation": "You can find links to external sources at the bottom of every {openverse} search results page; on pages for searches which return no results; and on pages for media types we do not yet support but intend to.", + "privacy.title": "Privacy", + "privacy.intro.content": "The {openverse} project seeks to make the privacy and safety of our users a priority. {openverse} adheres to the {link}. Please see that document for a full description of how {openverse} uses and protects any information that you give us.", + /** + * Interpolated into privacy.intro.content: + * _{link}_ part of "Openverse adheres to the _{link}_." */ + + "privacy.intro.link": "privacy policy of all WordPress.org websites", + "privacy.cookies.title": "Cookies", + /** privacy.cookies.content.a-b are parts of a single statement */ + "privacy.cookies.content.a": "{openverse} uses cookies to store information about visitor's preferences and information about their web browsers. We use this information to improve the user experience of the site.", + /** privacy.cookies.content.a-b are parts of a single statement */ + "privacy.cookies.content.b": 'These are considered "Necessary" or "Strictly necessary cookies". You may disable these by changing your browser settings, but this may affect how {openverse} functions.', + "privacy.contact.title": "Contact Us", + "privacy.contact.content": "Any questions about {openverse} and privacy can be sent to {email}, shared as a {issue}, or discussed with our community in the #openverse channel of the {chat}.", + /** + * Interpolated into privacy.contact.content: + * _{issue}_ part of "Any questions about Openverse and privacy can be sent to openverse@wordpress.org, shared as a _{issue}_, or discussed with our community in the #openverse channel of the Making WordPress Chat." + */ + "privacy.contact.issue": "GitHub issue", + /** + * Interpolated into privacy.contact.content: + * _{chat}_ part of "Any questions about Openverse and privacy can be sent to openverse@wordpress.org, shared as a Github issue, or discussed with our community in the #openverse channel of the _{chat}_." + */ + "privacy.contact.chat": "Making WordPress Chat", + "searchGuide.title": "{openverse} Syntax Guide", + "searchGuide.intro": "When you search, you can enter special symbols or words to your search term to make your search results more precise.", + "searchGuide.exact.title": "Search for an exact match", + "searchGuide.exact.ariaLabel": "quote unquote Claude Monet", + "searchGuide.exact.claudeMonet": '"Claude Monet"', + "searchGuide.exact.content": "To search for an exact word or phrase, put it inside quotes. For example, {link}.", + "searchGuide.negate.title": "Excluding terms", + /** + * Interpolated into searchGuide.negate.content: + * _{operator}_ part of 'To exclude a term from your results, put the {operator} in front of it.' + */ + "searchGuide.negate.operatorName": "minus sign", + "searchGuide.negate.ariaLabel": "dog minus pug", + "searchGuide.negate.example": "dog -pug", + "searchGuide.negate.content": 'To exclude a term from your results, put the {operator} in front of it. Example: {link}{br} This will search for media related to "dog" but won\'t include results related to "pug".', + "feedback.title": "Feedback", + "feedback.intro": "Thank you for using {openverse}! We welcome your ideas for improving the tool below. To provide regular feedback, join the {slack} channel in the {makingWordpress} Slack workspace.", + "feedback.improve": "Help us Improve", + "feedback.report": "Report a Bug", + "feedback.loading": "Loading...", + "feedback.aria.improve": "help us improve form", + "feedback.aria.report": "report a bug form", // translation keys used on the /sensitive-content page - sensitive: { - title: "Sensitive content", - description: { - content: { - /** sensitive.description.content.a-f are parts of a single statement */ - a: "{openverse} operates along a “safe-by-default” approach in all aspects of its operation and development, with the intention of being as inclusive and accessible as possible.", - /** sensitive.description.content.a-f are parts of a single statement */ - b: "Therefore, {openverse} only includes results with sensitive content when users have explicitly opted in to the “include sensitive results” features on {openverseOrg} and in the {openverse} API.", - /** - * sensitive.description.content.a-are parts of a single statement f. - * "wpCoc" and "deiStatement" are interpolated with sensitive.description.wpCoc and sensitive.description.deiStatement respectively. - */ - c: "In adherence to {wpCoc} and its {deiStatement}, {openverse} holds contributors to high expectations regarding conduct towards other contributors, the accessibility of contribution and the services, and, therefore, being an inclusive project.", - /** sensitive.description.content.a-f are parts of a single statement */ - d: "Similarly, {openverse} holds the expectation that the results returned from the API or displayed on the {openverseOrg} website should be accessible by default.", - /** sensitive.description.content.a-f are parts of a single statement */ - e: "Everyone, regardless of background, should feel safe and included in {openverse}, whether they are a contributor to the technical aspects of the {openverse} services, a creator whose works are included in {openverse}, or an {openverse} user.", - /** sensitive.description.content.a-f are parts of a single statement */ - f: "{openverse} recognises its responsibility as a tool used by people of a wide variety of ages, including young people in educational settings, and pays particular attention to minimizing accidental interaction with or exposure to sensitive content.", - }, - /** - * Interpolated into sensitive.description.content.c. - * Referencing https://make.wordpress.org/handbook/community-code-of-conduct/ - */ - wpCoc: "WordPress’s Community Code of Conduct", - /** - * Interpolated into sensitive.description.content.c: - _{deiStatement}_ part of "In adherence to WordPress’s Community Code of Conduct and its _{deiStatement}_, Openverse holds contributors to high expectations regarding conduct towards other contributors, the accessibility of contribution and the services, and, therefore, being an inclusive project." - * Referencing https://make.wordpress.org/handbook/diversity-equity-and-inclusion-in-wordpress/ - */ - deiStatement: "diversity, equity, and inclusion statement", - }, - sensitivity: { - what: { - /** sensitive.sensitivity.what.a-d are parts of a single statement */ - a: '{openverse} uses the term "sensitive" rather than "mature", "NSFW" (not safe for work), or other terms in order to indicate that our designation of content as sensitive is broad, with a focus on accessibility and inclusion.', - /** sensitive.sensitivity.what.a-d are parts of a single statement */ - b: 'This means that some content is designated "sensitive" that would not fall into a category of what is generally understood to be "mature" content (in other words, content specifically for an adult audience).', - /** sensitive.sensitivity.what.a-d are parts of a single statement */ - c: "The designation does not, however, imply that {openverse} or its maintainers view the content as inappropriate for the platform in general and is likewise not an implication of moral or ethical judgement.", - /** sensitive.sensitivity.what.a-d are parts of a single statement */ - d: 'We consider "sensitive" content to be content that is offensive, disturbing, graphic, or otherwise inappropriate, with particular attention paid to young people.', - }, - how: { - /** Sensitive.sensitivity.how.a-c are parts of a single statement */ - a: "This definition of sensitivity has a tremendous degree of flexibility and is intentionally imprecise.", - /** Sensitive.sensitivity.how.a-c are parts of a single statement */ - b: "{openverse} relies on a variety of tools to discover potentially sensitive content, including moderated user reports on individual work and scanning the textual content related to a work for sensitive terms.", - /** Sensitive.sensitivity.how.a-c are parts of a single statement */ - c: "These are described in more detail below.", - }, - }, - onOff: { - title: "Turning sensitive content on and off", - sensitiveResults: "By default, {openverse} does not include sensitive content in search results. Inclusion of sensitive results requires an explicit opt-in from the user. The user can opt-in to include sensitive content in the search results by enabling the “Sensitive results” switch.", - blurSensitive: { - /** sensitive.onOff.blurSensitive.a-b are parts of a single statement */ - a: "When sensitive content is included, the sensitive results returned are also blurred to prevent accidental exposure.", - /** sensitive.onOff.blurSensitive.a-b are parts of a single statement */ - b: "Unblurring them also requires an explicit opt-in from the user. The user can opt-in to see unblurred sensitive content by disabling the “Blur content” switch.", - }, - where: "Both these toggles are available in the filter sidebar (on desktops) and in the “Filter” tab of the search settings pane (on mobile devices) on the search results page.", - }, - designations: { - title: "Sensitive content designations", - description: { - /** sensitive.designations.description.a-b are parts of a single statement */ - a: "{openverse} designates sensitive content in the API and on the {openverseOrg} website using two methods: reports from {openverse} users and automated sensitive textual content detection.", - /** sensitive.designations.description.a-b are parts of a single statement */ - b: "These designations are not exclusive of each other and a single work may have one or both applied to it.", - }, - userReported: { - title: "User reported sensitivity", - description: { - /** sensitive.designations.userReported.description.a-d are parts of a single statement */ - a: "{openverse} users are invited to report sensitive content via the {openverseOrg} website and the {openverse} API.", - /** sensitive.designations.userReported.description.a-d are parts of a single statement */ - b: "Some tools and apps that integrate with the {openverse} API, like the {gutenbergMediaInserter}, also allow their users to report sensitive content.", - /** sensitive.designations.userReported.description.a-d are parts of a single statement */ - c: "An individual work’s page includes the ability to report content as sensitive (or to report rights violations).", - /** sensitive.designations.userReported.description.a-d are parts of a single statement */ - d: "{openverse} moderators check these reports and make decisions about whether to add a sensitivity designation to the work or, in certain cases as described above, delist the work from {openverse}’s services.", - }, - /** - * Interpolated into sensitive.designations.userReported.description.b, - * _{gutenbergMediaInserter}_ part of "Some tools and apps that integrate with the Openverse API, like the _{gutenbergMediaInserter}_, also allow their users to report sensitive content." - */ - gutenbergMediaInserter: "Gutenberg editor’s {openverse} media inserter", - }, - sensitiveText: { - title: "Sensitive textual content", - description: { - /** sensitive.designations.sensitiveText.description.a-e are a single statement */ - a: "{openverse} scans some of the textual metadata related to works as provided by our sources for sensitive terms.", - /** sensitive.designations.sensitiveText.description.a-e are a single statement */ - b: "{openverse}’s {sensitiveTermsList} is open source and contributions and input from the community are welcome and invited.", - /** sensitive.designations.sensitiveText.description.a-e are a single statement */ - c: "Examples of potentially sensitive text include but are not limited to text of a sexual, biological, violent, racist, or otherwise derogatory nature.", - /** sensitive.designations.sensitiveText.description.a-e are a single statement */ - d: "The project recognises that this approach is imperfect and that some works may inadvertently receive a sensitivity designation without necessarily being sensitive.", - /** sensitive.designations.sensitiveText.description.a-e are a single statement */ - e: "For more context on why we’ve chosen this approach despite that, refer to the {imperfect} of our project planning document related to this feature.", - }, - /** - * Interpolated into sensitive.designations.sensitiveText.description.b: - * _{sensitiveTermsList}_ part of "Openverse’s {sensitiveTermsList} is open source and contributions and input from the community are welcome and invited." - */ - sensitiveTermsList: "sensitive terms list", - /** - * Interpolated into sensitive.designations.sensitiveText.description.e: - * _{imperfect}_ part of "For more context on why we’ve chosen this approach despite that, refer to the _{imperfect}_ of our project planning document related to this feature." - * {sectionName} is replaced with "This will not be perfect" - */ - imperfect: '"{sectionName}" section', - metadata: { - /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ - a: "It is important to note that some textual metadata for a work is {notAvailable} through the {openverse} API or the {openverseOrg} website.", - /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ - b: "However, such metadata is still scanned for sensitive terms and is not treated as a special case.", - /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ - c: "If {openverse}’s text scanning finds sensitive terms in those metadata fields for a work, the work will still receive a sensitivity designation based on sensitive text even though the sensitive text itself is not available through {openverse}.", - /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ - d: "{openverse} takes the approach that sensitive textual content in a description is a relatively high correlative indicator of potentially sensitive works.", - /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ - e: "As above, {openverse} understands that this is not perfect.", - }, - /** - * Interpolated into sensitive.designations.sensitiveText.metadata.a: - * _{notAvailable}_ part of "It is important to note that some textual metadata for a work is _{notAvailable}_ through the Openverse API or the openverse.org website." - */ - notAvailable: "not available", - }, - }, - faq: { - title: "Frequently asked questions", - one: { - question: "I’ve found content I think is sensitive that does not have a sensitivity designation. What should I do?", - answer: { - /** sensitive.faq.one.answer.a-b are a single statement */ - a: "Please report sensitive content by visiting the individual work’s page on the {openverseOrg} website and using the “report this content” button below the attribution information and above the tags.", - /** sensitive.faq.one.answer.a-b are a single statement */ - b: "{openverse} moderates reports individually and reserves the right to respectfully decline the request to add a sensitivity designation to a given work.", - }, - }, - two: { - question: "I disagree with the sensitivity designation on a work. Can you please remove it?", - answerPt1: "For text-based designations, {openverse} does not at the moment have a method for removing the designation. This is a feature that will be built eventually, but is not part of the baseline sensitive content detection feature.", - answerPt2: { - /** sensitive.faq.two.a-c are a single */ - a: "For user reported designations, please file a new report on the work’s page following the instructions in the previous question.", - /** sensitive.faq.two.a-c are a single */ - b: "In the notes, describe why you believe the work should not have a sensitivity designation.", - /** sensitive.faq.two.a-c are a single */ - c: "As when adding a new designation, {openverse} reserves the right to respectfully decline the request to remove a confirmed user sensitivity designation.", - }, - }, - three: { - question: "I’ve found content on {openverse} that may be illegal. Besides reporting it to {openverse}, are there any other steps I can take?", - answer: { - /** sensitive.faq.three.answer.a-c are a single statement */ - a: "For user reported designations, please file a new report on the work’s page following the instructions in the previous question.", - /** sensitive.faq.three.answer.a-c are a single statement */ - b: "In the notes, describe why you believe the work should not have a sensitivity designation.", - /** sensitive.faq.three.answer.a-c are a single statement */ - c: "As when adding a new designation, {openverse} reserves the right to respectfully decline the request to remove a confirmed user sensitivity designation.", - }, - }, - }, - }, - tags: { - title: "Understanding Tags in {openverse}", - /** generatedTags.intro.a-b are parts of a single section explaining how tags work in Openverse. */ - intro: { - a: "Each creative work in {openverse} may have tags, an optional set of keywords used to describe the work and make it easier for users to find relevant media for their searches.", - b: "These tags fall into two main categories: source tags and generated tags. Understanding the difference between them can enhance your search experience and improve the accuracy of your results.", - }, - sourceTags: { - title: "Source Tags", - /** sourceTags.content.a-b are parts of a single section explaining how source tags work in Openverse. */ - content: { - a: "Source tags are tags that originate from the original source of the creative work. These tags may be added by different contributors, for example a photographer who uploaded their image to Flickr and added descriptive tags.", - b: "The original platform itself may assign additional tags from community members, automation, or other sources.", - }, - }, - generatedTags: { - title: "Generated Tags", - /** generatedTags.content.a-d are parts of a single section explaining how generated tags work in Openverse. */ - content: { - a: "Generated tags are created through automated machine analysis of creative works, most commonly images. This process involves advanced technologies like AWS Rekognition, Clarifai, and other image recognition services that analyze the content and generate descriptive tags. ", - b: "While generally reliable, automated systems can sometimes misinterpret or miss elements in an image.", - c: "Openverse makes efforts to exclude any generated tags that make inferences about the identities or affiliations of human subjects. ", - d: 'If you encounter any images with generated tags making assumptions about, for example, gender, religion, or political affiliation, please report the images using the "Report" button on our single result pages.', - }, - }, - }, - error: { - occurred: "An error occurred", - imageNotFound: "Couldn't find image with id {id}", - mediaNotFound: "Couldn't find {mediaType} with id {id}", - /** - * Interpolated into error.mediaNotFound: - * _{mediaType}_ part of "Couldn't find {mediaType} with id {id}" - */ - image: "image", - /** - * Interpolated into error.mediaNotFound: - * _{mediaType}_ part of "Couldn't find {mediaType} with id {id}" - */ - audio: "an audio track", - }, - filters: { - title: "Filters", - filterBy: "Filter By", - licenses: { - title: "Licenses", - cc0: "CC0", - pdm: "Public Domain Mark", - by: "BY", - bySa: "BY-SA", - byNc: "BY-NC", - byNd: "BY-ND", - byNcSa: "BY-NC-SA", - byNcNd: "BY-NC-ND", - }, - licenseTypes: { - title: "Use", - commercial: "Use commercially", - modification: "Modify or adapt", - }, - imageProviders: { - title: "Source", - }, - audioProviders: { - title: "Source", - }, - audioCategories: { - title: "Audio category", - audiobook: "Audiobook", - music: "Music", - news: "News", - podcast: "Podcast", - pronunciation: "Pronunciation", - sound_effect: "Sound effects", - sound: "Sound effects", - }, - imageCategories: { - title: "Image type", - photograph: "Photographs", - illustration: "Illustrations", - digitized_artwork: "Digitized Artworks", - }, - audioExtensions: { - title: "Extension", - flac: "FLAC", - mid: "MID", - mp3: "MP3", - oga: "OGA", - ogg: "OGG", - opus: "OPUS", - wav: "WAV", - webm: "WEBM", - }, - imageExtensions: { - title: "Extension", - jpg: "JPEG", - png: "PNG", - gif: "GIF", - svg: "SVG", - }, - aspectRatios: { - title: "Aspect ratio", - tall: "Tall", - wide: "Wide", - square: "Square", - }, - sizes: { - title: "Image size", - small: "Small", - medium: "Medium", - large: "Large", - }, - safeBrowsing: { - title: "Safe Browsing", - desc: "Content marked as {sensitive} is not shown by default.", - /** - * Interpolated into filters.safeBrowsing.desc: - * _{sensitive}_ part of "Content marked as _{sensitive}_ is not shown by default." - */ - sensitive: "sensitive", - toggles: { - fetchSensitive: { - title: "Sensitive results", - desc: "Show results marked as sensitive in the results area.", - }, - blurSensitive: { - title: "Blur content", - desc: "Blur images and texts to prevent seeing sensitive material.", - }, - }, - }, - lengths: { - title: "Duration", - shortest: "< 30 sec", - short: "30 sec-2 min", - medium: "2-10 min", - long: "> 10 min", - }, - creator: { - title: "Search by Creator", - }, - searchBy: { - title: "Search By", - creator: "Creator", - }, - licenseExplanation: { - licenseDefinition: "License definition", - markDefinition: "{mark} definition", - more: { - license: "{readMore} about this license.", - mark: "{readMore} about {mark}.", - /** - * Interpolated into filters.licenseExplanation.more.license and filters.licenseExplanation.more.mark: - * _{readMore}_ part of "_{readMore}_ about this license." and _"{readMore}_ about CC0." - */ - readMore: "Read more", - }, - }, - aria: { - removeFilter: "Remove {label} filter", - }, - }, - filterList: { - filterBy: "Filter by", - hide: "Hide filters", - clear: "Clear filters", - clearNumbered: "Clear filters ({number})", - show: "Show results", - categoryAria: "filters list for {categoryName} category", - }, - browsePage: { - allNoResults: "No results", - allResultCount: "{localeCount} result|{localeCount} results", - allResultCountMore: "Top {localeCount} results", - contentLink: { - image: { - zero: "No images found for {query}.", - count: "See {localeCount} image found for {query}.|See {localeCount} images found for {query}.", - countMore: "See the top {localeCount} images found for {query}.", - }, - audio: { - zero: "No audio tracks found for {query}.", - count: "See {localeCount} audio tracks found for {query}.", - countMore: "See the top {localeCount} audio tracks found for {query}.", - }, - }, - load: "Load more results", - loading: "Loading...", - fetchingError: "Error fetching {type}:", - searchRating: { - content: "Are these results relevant?", - yes: "Yes", - no: "No", - feedbackThanks: "Thank you for the feedback!", - }, - searchForm: { - placeholder: "Search all {type}", - /** - * Interpolated into browsePage.searchForm.placeholder: - * _{type}_ part of "Search all {type}" - */ - image: "images", - /** - * Interpolated into browsePage.searchForm.placeholder: - * _{type}_ part of "Search all {type}" - */ - audio: "audio tracks", - /** - * Interpolated into browsePage.searchForm.placeholder: - * _{type}_ part of "Search all {type}" - */ - video: "videos", - /** - * Interpolated into browsePage.searchForm.placeholder: - * _{type}_ part of "Search all {type}" - */ - model3d: "3D Models", - /** - * Interpolated into browsePage.searchForm.placeholder: - * _{type}_ part of "Search all {type}" - */ - all: "content", - collectionPlaceholder: "Search this collection", - button: "Search", - clear: "Clear", - }, - licenseDescription: { - title: "License CC", - by: "Credit the creator.", - nc: "Noncommercial uses only.", - nd: "No derivatives or adaptations permitted.", - sa: "Share adaptations under the same terms.", - zero: "This work has been marked as dedicated to the public domain.", - pd: "This work is marked as being in the public domain.", - samplingPlus: "Samples, mash-ups, creative transformations permitted.", - }, - aria: { - close: "close", - scroll: "scroll to top", - search: "search", - removeFilter: "remove filter", - licenseExplanation: "license explanation", - creator: "search by creator", - imageTitle: "Image: {title}", - audioTitle: "Audio track: {title}", - /** - * These strings are used as aria-label of the list of the search results. - * The number of results is given in `results.mediaType`, so is not - * needed here. - */ - resultsLabel: { - all: "All results for {query}", - image: "Image results for {query}", - audio: "Audio tracks for {query}", - }, - results: { - /** - * "imageResults" and "audioResults" are interpolated from the strings under - * browsePage.aria.allResultsHeadingCount.* - */ - all: 'All results for "{query}", {imageResults} and {audioResults}.', - image: { - zero: 'No image results for "{query}"', - count: '{localeCount} image result for "{query}".|{localeCount} image results for "{query}".', - countMore: 'Top {localeCount} image results for "{query}".', - }, - audio: { - zero: 'No audio tracks for "{query}"', - count: '{localeCount} audio track for "{query}".|{localeCount} audio tracks for "{query}".', - countMore: 'Top {localeCount} audio tracks for "{query}".', - }, - }, - allResultsHeadingCount: { - image: { - /* Interpolated into browsePage.aria.results.all */ - zero: "no images", - /* Interpolated into browsePage.aria.results.all */ - count: "{localeCount} image|{localeCount} images", - /* Interpolated into browsePage.aria.results.all */ - countMore: "top {localeCount} images", - }, - audio: { - /* Interpolated into browsePage.aria.results.all */ - zero: "no audio tracks", - /* Interpolated into browsePage.aria.results.all */ - count: "{localeCount} audio track|{localeCount} audio tracks", - /* Interpolated into browsePage.aria.results.all */ - countMore: "top {localeCount} audio tracks", - }, - }, - }, - }, - mediaDetails: { - information: { - type: "Type", - unknown: "Unknown", - category: "Category", - }, - scroll: { - forward: "Scroll forward", - back: "Scroll backward", - }, - reuse: { - title: "How to use", - description: "Visit the {media}'s website to download and use it. Make sure to credit the creator by showing the attribution information where you are sharing your work.", - copyrightDisclaimer: "Some photographs might contain copyrighted content, such as paintings, sculptures, or architectural works. Using these photographs may require additional permissions from the copyright holder of the depicted works.", - licenseHeader: "License", - toolHeader: "Public Domain", - audio: "Audio track", - image: "Image", - tool: { - content: "Read more about the tool {link}.", - /** - * Interpolated into mediaDetails.reuse.tool.content: - * _{link}_ part of "Read more about the tool {link}." - */ - link: "here", - }, - credit: { - /** - * Interpolated into mediaDetails.reuse.credit.text: - * _{title}_ part of "_{title}_ by creator is licensed with CC0 1.0." - */ - genericTitle: "This work", - actualTitle: '"{title}"', - text: "{title} {creator} {markedLicensed} {license}. {viewLegal}", - /** - * Interpolated into mediaDetails.reuse.credit.text: - * _{creator}_ part of "This work _{creator}_ is licensed with CC0 1.0." - */ - creatorText: "by {creatorName}", - /** - * Interpolated into mediaDetails.reuse.credit.text: - * _{markedLicensed}_ part of "This work by creator _{markedLicensed}_ CC0 1.0." - */ - marked: "is marked with", - /** - * Interpolated into mediaDetails.reuse.credit.text: - * _{markedLicensed}_ part of "This work by creator _{markedLicensed}_ CC BY 4.0." - */ - licensed: "is licensed under", - viewLegalText: "To view {termsCopy}, visit {url}.", - /** - * Interpolated into mediaDetails.reuse.credit.viewLegalText: - * _{termsCopy}_ part of "To view _{termsCopy}_, visit {url}." - */ - termsText: "the terms", - /** - * Interpolated into mediaDetails.reuse.credit.viewLegalText: - * _{termsCopy}_ part of "To view {termsCopy}, visit {url}." - */ - copyText: "a copy of this license", - }, - copyLicense: { - title: "Credit the creator", - rich: "Rich Text", - html: "HTML", - plain: "Plain text", - copyText: "Copy text", - copied: "Copied!", - xml: "XML", - }, - attribution: "This image was marked with a {link} license:", - }, - providerLabel: "Provider", - sourceLabel: "Source", - providerDescription: "Website where the content is hosted", - sourceDescription: "Organization that created or owns the original content", - loading: "Loading...", - relatedError: "Error fetching related media", - aria: { - attribution: { - license: "read more about the license", - tool: "read more about the tool", - }, - creatorUrl: "author {name}", - }, - imageInfo: "Image information", - audioInfo: "Audio track information", - tags: { - title: "Tags", - generated: { - heading: "Generated tags", - pageTitle: "Learn more", - }, - source: { - heading: "Source tags", - }, - showMore: "Show more", - showLess: "Show less", - }, - contentReport: { - short: "Report", - long: "Report this content", - form: { - disclaimer: "For security purposes, {openverse} collects and retains anonymized IP addresses of those who complete and submit this form.", - question: "What is the reason?", - dmca: { - option: "Infringes copyright", - note: "You must fill out this {form} to report copyright infringement. No action will be taken until this form is filled out and submitted. We recommend doing the same at the source, {source}.", - /** - * Interpolated into mediaDetails.contentReport.dmca.note: - * _{form}_ part of "You must fill out this {form} to report copyright infringement." - */ - form: "DMCA form", - open: "Open form", - }, - sensitive: { - option: "Contains sensitive content", - subLabel: "Optional", - placeholder: "Optionally, provide a description.", - }, - other: { - option: "Other", - note: "Describe the issue.", - subLabel: "Required", - placeholder: "Please enter at least 20 characters.", - }, - submit: "Report", - cancel: "Cancel", - }, - success: { - title: "Report submitted successfully", - note: "Thank you for reporting this content. We recommend doing the same at the source, {source}.", - }, - failure: { - title: "Report could not be submitted", - note: "Something went wrong, please try again after some time.", - }, - }, - }, - singleResult: { - back: "Back to results", - }, - imageDetails: { - creator: "by {name}", - weblink: "Get this image", - information: { - dimensions: "Dimensions", - pixels: "pixels", - sizeInPixels: "{width} × {height} pixels", - }, - relatedImages: "Related images", - aria: { - creatorUrl: "Author {creator}", - }, - }, - audioDetails: { - genreLabel: "Genre", - relatedAudios: "Related audio tracks", - table: { - album: "Album", - sampleRate: "Sample Rate", - filetype: "Format", - genre: "Genre", - }, - weblink: "Get this audio track", - }, - allResults: { - snackbar: { - text: "Press {spacebar} to play or pause the track.", - /** - * Interpolated into allResults.snackbar.text: - * _{spacebar}_ part of "Press {spacebar} to play or pause the track." - */ - spacebar: "Spacebar", - }, - }, - audioResults: { - snackbar: { - text: "Press {spacebar} to play or pause, and {left} & {right} to seek through the track.", - /** - * Interpolated into audioResults.snackbar.text: - * _{spacebar}_ part of "Press {spacebar} to play or pause, and ← & → to seek through the track." - */ - spacebar: "Spacebar", - left: "←", - right: "→", - }, - }, - externalSources: { - caption: "{openverse} can not guarantee the accuracy of license information. Always verify that the work is actually under a CC license.", - button: "Source list", - title: "External sources", - card: { - search: "Not finding what you're looking for? Try external sources", - caption: "Click on a source below to directly search other collections of CC-licensed images.{break}Please note that Use filters are not supported for Open Clip Art Library or Nappy.", - }, - form: { - supportedTitle: "Not finding what you're looking for? Search in external sources", - supportedTitleSm: "Search in external sources", - }, - }, - browsers: { - chrome: "Chrome", - firefox: "Firefox", - opera: "Opera", - edge: "Edge", - }, - waveform: { - label: "Audio seek bar", - currentTime: "{time} second|{time} seconds", - }, - audioThumbnail: { - alt: 'Cover art for "{title}" by {creator}', - }, - audioTrack: { - ariaLabel: "Audio: {title}", - ariaLabelInteractive: "Audio: {title} - interactive player - press the space bar to play and pause a preview of the audio track", - ariaLabelInteractiveSeekable: "Audio: {title} - interactive player - press the space bar to play and pause a preview of the audio track; use the left and right arrow keys to seek through the track.", - messages: { - err_aborted: "You aborted playback.", - err_network: "A network error occurred.", - err_decode: "Could not decode audio track.", - err_unallowed: "Reproduction not allowed.", - err_unknown: "An unexpected error has occurred. Try again in a few minutes or report the item if the issue persists.", - err_unsupported: "This audio format is not supported by your browser.", - loading: "Loading...", - }, - creator: "by {creator}", - close: "Close the audio player", - }, - playPause: { - play: "Play", - pause: "Pause", - replay: "Replay", - loading: "Loading", - }, - search: { - search: "Search", - searchBarLabel: "Search for content in {openverse}", - }, - licenseReadableNames: { - cc0: "Zero", - pdm: "Public Domain Mark", - by: "Attribution", - bySa: "Attribution-Share-Alike", - byNc: "Attribution-NonCommercial", - byNd: "Attribution-NoDerivatives", - byNcSa: "Attribution-NonCommercial-Share-Alike", - byNcNd: "Attribution-NonCommercial-NoDerivatives", - "sampling+": "Sampling Plus", - "ncSampling+": "NonCommercial Sampling Plus", - }, + "sensitive.title": "Sensitive content", + /** sensitive.description.content.a-f are parts of a single statement */ + "sensitive.description.content.a": "{openverse} operates along a “safe-by-default” approach in all aspects of its operation and development, with the intention of being as inclusive and accessible as possible.", + /** sensitive.description.content.a-f are parts of a single statement */ + "sensitive.description.content.b": "Therefore, {openverse} only includes results with sensitive content when users have explicitly opted in to the “include sensitive results” features on {openverseOrg} and in the {openverse} API.", + /** sensitive.description.content.a-f are parts of a single statement */ + "sensitive.description.content.c": "In adherence to {wpCoc} and its {deiStatement}, {openverse} holds contributors to high expectations regarding conduct towards other contributors, the accessibility of contribution and the services, and, therefore, being an inclusive project.", + /** sensitive.description.content.a-f are parts of a single statement */ + "sensitive.description.content.d": "Similarly, {openverse} holds the expectation that the results returned from the API or displayed on the {openverseOrg} website should be accessible by default.", + /** sensitive.description.content.a-f are parts of a single statement */ + "sensitive.description.content.e": "Everyone, regardless of background, should feel safe and included in {openverse}, whether they are a contributor to the technical aspects of the {openverse} services, a creator whose works are included in {openverse}, or an {openverse} user.", + /** sensitive.description.content.a-f are parts of a single statement */ + "sensitive.description.content.f": "{openverse} recognises its responsibility as a tool used by people of a wide variety of ages, including young people in educational settings, and pays particular attention to minimizing accidental interaction with or exposure to sensitive content.", + /** + * Interpolated into sensitive.description.content.c. + * Referencing https://make.wordpress.org/handbook/community-code-of-conduct/ + */ + "sensitive.description.wpCoc": "WordPress’s Community Code of Conduct", + /** + * Interpolated into sensitive.description.content.c: + _{deiStatement}_ part of "In adherence to WordPress’s Community Code of Conduct and its _{deiStatement}_, Openverse holds contributors to high expectations regarding conduct towards other contributors, the accessibility of contribution and the services, and, therefore, being an inclusive project." + * Referencing https://make.wordpress.org/handbook/diversity-equity-and-inclusion-in-wordpress/ + */ + "sensitive.description.deiStatement": "diversity, equity, and inclusion statement", + /** sensitive.sensitivity.what.a-d are parts of a single statement */ + "sensitive.sensitivity.what.a": '{openverse} uses the term "sensitive" rather than "mature", "NSFW" (not safe for work), or other terms in order to indicate that our designation of content as sensitive is broad, with a focus on accessibility and inclusion.', + /** sensitive.sensitivity.what.a-d are parts of a single statement */ + "sensitive.sensitivity.what.b": 'This means that some content is designated "sensitive" that would not fall into a category of what is generally understood to be "mature" content (in other words, content specifically for an adult audience).', + /** sensitive.sensitivity.what.a-d are parts of a single statement */ + "sensitive.sensitivity.what.c": "The designation does not, however, imply that {openverse} or its maintainers view the content as inappropriate for the platform in general and is likewise not an implication of moral or ethical judgement.", + /** sensitive.sensitivity.what.a-d are parts of a single statement */ + "sensitive.sensitivity.what.d": 'We consider "sensitive" content to be content that is offensive, disturbing, graphic, or otherwise inappropriate, with particular attention paid to young people.', + /** Sensitive.sensitivity.how.a-c are parts of a single statement */ + "sensitive.sensitivity.how.a": "This definition of sensitivity has a tremendous degree of flexibility and is intentionally imprecise.", + /** Sensitive.sensitivity.how.a-c are parts of a single statement */ + "sensitive.sensitivity.how.b": "{openverse} relies on a variety of tools to discover potentially sensitive content, including moderated user reports on individual work and scanning the textual content related to a work for sensitive terms.", + /** Sensitive.sensitivity.how.a-c are parts of a single statement */ + "sensitive.sensitivity.how.c": "These are described in more detail below.", + "sensitive.onOff.title": "Turning sensitive content on and off", + "sensitive.onOff.sensitiveResults": "By default, {openverse} does not include sensitive content in search results. Inclusion of sensitive results requires an explicit opt-in from the user. The user can opt-in to include sensitive content in the search results by enabling the “Sensitive results” switch.", + /** sensitive.onOff.blurSensitive.a-b are parts of a single statement */ + "sensitive.onOff.blurSensitive.a": "When sensitive content is included, the sensitive results returned are also blurred to prevent accidental exposure.", + /** sensitive.onOff.blurSensitive.a-b are parts of a single statement */ + "sensitive.onOff.blurSensitive.b": "Unblurring them also requires an explicit opt-in from the user. The user can opt-in to see unblurred sensitive content by disabling the “Blur content” switch.", + "sensitive.onOff.where": "Both these toggles are available in the filter sidebar (on desktops) and in the “Filter” tab of the search settings pane (on mobile devices) on the search results page.", + "sensitive.designations.title": "Sensitive content designations", + /** sensitive.designations.description.a-b are parts of a single statement */ + "sensitive.designations.description.a": "{openverse} designates sensitive content in the API and on the {openverseOrg} website using two methods: reports from {openverse} users and automated sensitive textual content detection.", + /** sensitive.designations.description.a-b are parts of a single statement */ + "sensitive.designations.description.b": "These designations are not exclusive of each other and a single work may have one or both applied to it.", + "sensitive.designations.userReported.title": "User reported sensitivity", + /** sensitive.designations.userReported.description.a-d are parts of a single statement */ + "sensitive.designations.userReported.description.a": "{openverse} users are invited to report sensitive content via the {openverseOrg} website and the {openverse} API.", + /** sensitive.designations.userReported.description.a-d are parts of a single statement */ + "sensitive.designations.userReported.description.b": "Some tools and apps that integrate with the {openverse} API, like the {gutenbergMediaInserter}, also allow their users to report sensitive content.", + /** sensitive.designations.userReported.description.a-d are parts of a single statement */ + "sensitive.designations.userReported.description.c": "An individual work’s page includes the ability to report content as sensitive (or to report rights violations).", + /** sensitive.designations.userReported.description.a-d are parts of a single statement */ + "sensitive.designations.userReported.description.d": "{openverse} moderators check these reports and make decisions about whether to add a sensitivity designation to the work or, in certain cases as described above, delist the work from {openverse}’s services.", + /** + * Interpolated into sensitive.designations.userReported.description.b, + * _{gutenbergMediaInserter}_ part of "Some tools and apps that integrate with the Openverse API, like the _{gutenbergMediaInserter}_, also allow their users to report sensitive content." + */ + "sensitive.designations.userReported.gutenbergMediaInserter": "Gutenberg editor’s {openverse} media inserter", + "sensitive.designations.sensitiveText.title": "Sensitive textual content", + /** sensitive.designations.sensitiveText.description.a-e are a single statement */ + "sensitive.designations.sensitiveText.description.a": "{openverse} scans some of the textual metadata related to works as provided by our sources for sensitive terms.", + /** sensitive.designations.sensitiveText.description.a-e are a single statement */ + "sensitive.designations.sensitiveText.description.b": "{openverse}’s {sensitiveTermsList} is open source and contributions and input from the community are welcome and invited.", + /** sensitive.designations.sensitiveText.description.a-e are a single statement */ + "sensitive.designations.sensitiveText.description.c": "Examples of potentially sensitive text include but are not limited to text of a sexual, biological, violent, racist, or otherwise derogatory nature.", + /** sensitive.designations.sensitiveText.description.a-e are a single statement */ + "sensitive.designations.sensitiveText.description.d": "The project recognises that this approach is imperfect and that some works may inadvertently receive a sensitivity designation without necessarily being sensitive.", + /** sensitive.designations.sensitiveText.description.a-e are a single statement */ + "sensitive.designations.sensitiveText.description.e": "For more context on why we’ve chosen this approach despite that, refer to the {imperfect} of our project planning document related to this feature.", + /** + * Interpolated into sensitive.designations.sensitiveText.description.b: + * _{sensitiveTermsList}_ part of "Openverse’s {sensitiveTermsList} is open source and contributions and input from the community are welcome and invited." + */ + "sensitive.designations.sensitiveText.sensitiveTermsList": "sensitive terms list", + /** + * Interpolated into sensitive.designations.sensitiveText.description.e: + * _{imperfect}_ part of "For more context on why we’ve chosen this approach despite that, refer to the _{imperfect}_ of our project planning document related to this feature." + * {sectionName} is replaced with "This will not be perfect" + */ + "sensitive.designations.sensitiveText.imperfect": '"{sectionName}" section', + /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ + "sensitive.designations.sensitiveText.metadata.a": "It is important to note that some textual metadata for a work is {notAvailable} through the {openverse} API or the {openverseOrg} website.", + /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ + "sensitive.designations.sensitiveText.metadata.b": "However, such metadata is still scanned for sensitive terms and is not treated as a special case.", + /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ + "sensitive.designations.sensitiveText.metadata.c": "If {openverse}’s text scanning finds sensitive terms in those metadata fields for a work, the work will still receive a sensitivity designation based on sensitive text even though the sensitive text itself is not available through {openverse}.", + /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ + "sensitive.designations.sensitiveText.metadata.d": "{openverse} takes the approach that sensitive textual content in a description is a relatively high correlative indicator of potentially sensitive works.", + /** sensitive.designations.sensitiveText.metadata.a-e are a single statement */ + "sensitive.designations.sensitiveText.metadata.e": "As above, {openverse} understands that this is not perfect.", + /** + * Interpolated into sensitive.designations.sensitiveText.metadata.a: + * _{notAvailable}_ part of "It is important to note that some textual metadata for a work is _{notAvailable}_ through the Openverse API or the openverse.org website." + */ + "sensitive.designations.sensitiveText.notAvailable": "not available", + "sensitive.faq.title": "Frequently asked questions", + "sensitive.faq.one.question": "I’ve found content I think is sensitive that does not have a sensitivity designation. What should I do?", + /** sensitive.faq.one.answer.a-b are a single statement */ + "sensitive.faq.one.answer.a": "Please report sensitive content by visiting the individual work’s page on the {openverseOrg} website and using the “report this content” button below the attribution information and above the tags.", + /** sensitive.faq.one.answer.a-b are a single statement */ + "sensitive.faq.one.answer.b": "{openverse} moderates reports individually and reserves the right to respectfully decline the request to add a sensitivity designation to a given work.", + "sensitive.faq.two.question": "I disagree with the sensitivity designation on a work. Can you please remove it?", + "sensitive.faq.two.answerPt1": "For text-based designations, {openverse} does not at the moment have a method for removing the designation. This is a feature that will be built eventually, but is not part of the baseline sensitive content detection feature.", + /** sensitive.faq.two.a-c are a single */ + "sensitive.faq.two.answerPt2.a": "For user reported designations, please file a new report on the work’s page following the instructions in the previous question.", + /** sensitive.faq.two.a-c are a single */ + "sensitive.faq.two.answerPt2.b": "In the notes, describe why you believe the work should not have a sensitivity designation.", + /** sensitive.faq.two.a-c are a single */ + "sensitive.faq.two.answerPt2.c": "As when adding a new designation, {openverse} reserves the right to respectfully decline the request to remove a confirmed user sensitivity designation.", + "sensitive.faq.three.question": "I’ve found content on {openverse} that may be illegal. Besides reporting it to {openverse}, are there any other steps I can take?", + /** sensitive.faq.three.answer.a-c are a single statement */ + "sensitive.faq.three.answer.a": "For user reported designations, please file a new report on the work’s page following the instructions in the previous question.", + /** sensitive.faq.three.answer.a-c are a single statement */ + "sensitive.faq.three.answer.b": "In the notes, describe why you believe the work should not have a sensitivity designation.", + /** sensitive.faq.three.answer.a-c are a single statement */ + "sensitive.faq.three.answer.c": "As when adding a new designation, {openverse} reserves the right to respectfully decline the request to remove a confirmed user sensitivity designation.", + "tags.title": "Understanding Tags in {openverse}", + /** generatedTags.intro.a-b are parts of a single section explaining how tags work in Openverse. */ + "tags.intro.a": "Each creative work in {openverse} may have tags, an optional set of keywords used to describe the work and make it easier for users to find relevant media for their searches.", + /** generatedTags.intro.a-b are parts of a single section explaining how tags work in Openverse. */ + "tags.intro.b": "These tags fall into two main categories: source tags and generated tags. Understanding the difference between them can enhance your search experience and improve the accuracy of your results.", + "tags.sourceTags.title": "Source Tags", + /** sourceTags.content.a-b are parts of a single section explaining how source tags work in Openverse. */ + "tags.sourceTags.content.a": "Source tags are tags that originate from the original source of the creative work. These tags may be added by different contributors, for example a photographer who uploaded their image to Flickr and added descriptive tags.", + /** sourceTags.content.a-b are parts of a single section explaining how source tags work in Openverse. */ + "tags.sourceTags.content.b": "The original platform itself may assign additional tags from community members, automation, or other sources.", + "tags.generatedTags.title": "Generated Tags", + /** generatedTags.content.a-d are parts of a single section explaining how generated tags work in Openverse. */ + "tags.generatedTags.content.a": "Generated tags are created through automated machine analysis of creative works, most commonly images. This process involves advanced technologies like AWS Rekognition, Clarifai, and other image recognition services that analyze the content and generate descriptive tags. ", + /** generatedTags.content.a-d are parts of a single section explaining how generated tags work in Openverse. */ + "tags.generatedTags.content.b": "While generally reliable, automated systems can sometimes misinterpret or miss elements in an image.", + /** generatedTags.content.a-d are parts of a single section explaining how generated tags work in Openverse. */ + "tags.generatedTags.content.c": "Openverse makes efforts to exclude any generated tags that make inferences about the identities or affiliations of human subjects. ", + /** generatedTags.content.a-d are parts of a single section explaining how generated tags work in Openverse. */ + "tags.generatedTags.content.d": 'If you encounter any images with generated tags making assumptions about, for example, gender, religion, or political affiliation, please report the images using the "Report" button on our single result pages.', + "error.occurred": "An error occurred", + "error.imageNotFound": "Couldn't find image with id {id}", + "error.mediaNotFound": "Couldn't find {mediaType} with id {id}", + /** + * Interpolated into error.mediaNotFound: + * _{mediaType}_ part of "Couldn't find {mediaType} with id {id}" + */ + "error.image": "image", + /** + * Interpolated into error.mediaNotFound: + * _{mediaType}_ part of "Couldn't find {mediaType} with id {id}" + */ + "error.audio": "an audio track", + "filters.title": "Filters", + "filters.filterBy": "Filter By", + "filters.licenses.title": "Licenses", + "filters.licenses.cc0": "CC0", + "filters.licenses.pdm": "Public Domain Mark", + "filters.licenses.by": "BY", + "filters.licenses.bySa": "BY-SA", + "filters.licenses.byNc": "BY-NC", + "filters.licenses.byNd": "BY-ND", + "filters.licenses.byNcSa": "BY-NC-SA", + "filters.licenses.byNcNd": "BY-NC-ND", + "filters.licenseTypes.title": "Use", + "filters.licenseTypes.commercial": "Use commercially", + "filters.licenseTypes.modification": "Modify or adapt", + "filters.imageProviders.title": "Source", + "filters.audioProviders.title": "Source", + "filters.audioCategories.title": "Audio category", + "filters.audioCategories.audiobook": "Audiobook", + "filters.audioCategories.music": "Music", + "filters.audioCategories.news": "News", + "filters.audioCategories.podcast": "Podcast", + "filters.audioCategories.pronunciation": "Pronunciation", + "filters.audioCategories.sound_effect": "Sound effects", + "filters.audioCategories.sound": "Sound effects", + "filters.imageCategories.title": "Image type", + "filters.imageCategories.photograph": "Photographs", + "filters.imageCategories.illustration": "Illustrations", + "filters.imageCategories.digitized_artwork": "Digitized Artworks", + "filters.audioExtensions.title": "Extension", + "filters.audioExtensions.flac": "FLAC", + "filters.audioExtensions.mid": "MID", + "filters.audioExtensions.mp3": "MP3", + "filters.audioExtensions.oga": "OGA", + "filters.audioExtensions.ogg": "OGG", + "filters.audioExtensions.opus": "OPUS", + "filters.audioExtensions.wav": "WAV", + "filters.audioExtensions.webm": "WEBM", + "filters.imageExtensions.title": "Extension", + "filters.imageExtensions.jpg": "JPEG", + "filters.imageExtensions.png": "PNG", + "filters.imageExtensions.gif": "GIF", + "filters.imageExtensions.svg": "SVG", + "filters.aspectRatios.title": "Aspect ratio", + "filters.aspectRatios.tall": "Tall", + "filters.aspectRatios.wide": "Wide", + "filters.aspectRatios.square": "Square", + "filters.sizes.title": "Image size", + "filters.sizes.small": "Small", + "filters.sizes.medium": "Medium", + "filters.sizes.large": "Large", + "filters.safeBrowsing.title": "Safe Browsing", + "filters.safeBrowsing.desc": "Content marked as {sensitive} is not shown by default.", + /** + * Interpolated into filters.safeBrowsing.desc: + * _{sensitive}_ part of "Content marked as _{sensitive}_ is not shown by default." + */ + "filters.safeBrowsing.sensitive": "sensitive", + "filters.safeBrowsing.toggles.fetchSensitive.title": "Sensitive results", + "filters.safeBrowsing.toggles.fetchSensitive.desc": "Show results marked as sensitive in the results area.", + "filters.safeBrowsing.toggles.blurSensitive.title": "Blur content", + "filters.safeBrowsing.toggles.blurSensitive.desc": "Blur images and texts to prevent seeing sensitive material.", + "filters.lengths.title": "Duration", + "filters.lengths.shortest": "< 30 sec", + "filters.lengths.short": "30 sec-2 min", + "filters.lengths.medium": "2-10 min", + "filters.lengths.long": "> 10 min", + "filters.creator.title": "Search by Creator", + "filters.searchBy.title": "Search By", + "filters.searchBy.creator": "Creator", + "filters.licenseExplanation.licenseDefinition": "License definition", + "filters.licenseExplanation.markDefinition": "{mark} definition", + "filters.licenseExplanation.more.license": "{readMore} about this license.", + "filters.licenseExplanation.more.mark": "{readMore} about {mark}.", + /** + * Interpolated into filters.licenseExplanation.more.license and filters.licenseExplanation.more.mark: + * _{readMore}_ part of "_{readMore}_ about this license." and _"{readMore}_ about CC0." + */ + "filters.licenseExplanation.more.readMore": "Read more", + "filters.aria.removeFilter": "Remove {label} filter", + "filterList.filterBy": "Filter by", + "filterList.hide": "Hide filters", + "filterList.clear": "Clear filters", + "filterList.clearNumbered": "Clear filters ({number})", + "filterList.show": "Show results", + "filterList.categoryAria": "filters list for {categoryName} category", + "browsePage.allNoResults": "No results", + "browsePage.allResultCount": "{localeCount} result|{localeCount} results", + "browsePage.allResultCountMore": "Top {localeCount} results", + "browsePage.contentLink.image.zero": "No images found for {query}.", + "browsePage.contentLink.image.count": "See {localeCount} image found for {query}.|See {localeCount} images found for {query}.", + "browsePage.contentLink.image.countMore": "See the top {localeCount} images found for {query}.", + "browsePage.contentLink.audio.zero": "No audio tracks found for {query}.", + "browsePage.contentLink.audio.count": "See {localeCount} audio tracks found for {query}.", + "browsePage.contentLink.audio.countMore": "See the top {localeCount} audio tracks found for {query}.", + "browsePage.load": "Load more results", + "browsePage.loading": "Loading...", + "browsePage.fetchingError": "Error fetching {type}:", + "browsePage.searchRating.content": "Are these results relevant?", + "browsePage.searchRating.yes": "Yes", + "browsePage.searchRating.no": "No", + "browsePage.searchRating.feedbackThanks": "Thank you for the feedback!", + "browsePage.searchForm.placeholder": "Search all {type}", + /** + * Interpolated into browsePage.searchForm.placeholder: + * _{type}_ part of "Search all {type}" + */ + "browsePage.searchForm.image": "images", + /** + * Interpolated into browsePage.searchForm.placeholder: + * _{type}_ part of "Search all {type}" + */ + "browsePage.searchForm.audio": "audio tracks", + /** + * Interpolated into browsePage.searchForm.placeholder: + * _{type}_ part of "Search all {type}" + */ + "browsePage.searchForm.video": "videos", + /** + * Interpolated into browsePage.searchForm.placeholder: + * _{type}_ part of "Search all {type}" + */ + "browsePage.searchForm.model3d": "3D Models", + /** + * Interpolated into browsePage.searchForm.placeholder: + * _{type}_ part of "Search all {type}" + */ + "browsePage.searchForm.all": "content", + "browsePage.searchForm.collectionPlaceholder": "Search this collection", + "browsePage.searchForm.button": "Search", + "browsePage.searchForm.clear": "Clear", + "browsePage.licenseDescription.title": "License CC", + "browsePage.licenseDescription.by": "Credit the creator.", + "browsePage.licenseDescription.nc": "Noncommercial uses only.", + "browsePage.licenseDescription.nd": "No derivatives or adaptations permitted.", + "browsePage.licenseDescription.sa": "Share adaptations under the same terms.", + "browsePage.licenseDescription.zero": "This work has been marked as dedicated to the public domain.", + "browsePage.licenseDescription.pd": "This work is marked as being in the public domain.", + "browsePage.licenseDescription.samplingPlus": "Samples, mash-ups, creative transformations permitted.", + "browsePage.aria.close": "close", + "browsePage.aria.scroll": "scroll to top", + "browsePage.aria.search": "search", + "browsePage.aria.removeFilter": "remove filter", + "browsePage.aria.licenseExplanation": "license explanation", + "browsePage.aria.creator": "search by creator", + "browsePage.aria.imageTitle": "Image: {title}", + "browsePage.aria.audioTitle": "Audio track: {title}", + /** + * These strings are used as aria-label of the list of the search results. + * The number of results is given in `results.mediaType`, so is not + * needed here. + */ + "browsePage.aria.resultsLabel.all": "All results for {query}", + "browsePage.aria.resultsLabel.image": "Image results for {query}", + "browsePage.aria.resultsLabel.audio": "Audio tracks for {query}", + /** + * "imageResults" and "audioResults" are interpolated from the strings under + * browsePage.aria.allResultsHeadingCount.* + */ + "browsePage.aria.results.all": 'All results for "{query}", {imageResults} and {audioResults}.', + "browsePage.aria.results.image.zero": 'No image results for "{query}"', + "browsePage.aria.results.image.count": '{localeCount} image result for "{query}".|{localeCount} image results for "{query}".', + "browsePage.aria.results.image.countMore": 'Top {localeCount} image results for "{query}".', + "browsePage.aria.results.audio.zero": 'No audio tracks for "{query}"', + "browsePage.aria.results.audio.count": '{localeCount} audio track for "{query}".|{localeCount} audio tracks for "{query}".', + "browsePage.aria.results.audio.countMore": 'Top {localeCount} audio tracks for "{query}".', + /* Interpolated into browsePage.aria.results.all */ + "browsePage.aria.allResultsHeadingCount.image.zero": "no images", + /* Interpolated into browsePage.aria.results.all */ + "browsePage.aria.allResultsHeadingCount.image.count": "{localeCount} image|{localeCount} images", + /* Interpolated into browsePage.aria.results.all */ + "browsePage.aria.allResultsHeadingCount.image.countMore": "top {localeCount} images", + /* Interpolated into browsePage.aria.results.all */ + "browsePage.aria.allResultsHeadingCount.audio.zero": "no audio tracks", + /* Interpolated into browsePage.aria.results.all */ + "browsePage.aria.allResultsHeadingCount.audio.count": "{localeCount} audio track|{localeCount} audio tracks", + /* Interpolated into browsePage.aria.results.all */ + "browsePage.aria.allResultsHeadingCount.audio.countMore": "top {localeCount} audio tracks", + "mediaDetails.information.type": "Type", + "mediaDetails.information.unknown": "Unknown", + "mediaDetails.information.category": "Category", + "mediaDetails.scroll.forward": "Scroll forward", + "mediaDetails.scroll.back": "Scroll backward", + "mediaDetails.reuse.title": "How to use", + "mediaDetails.reuse.description": "Visit the {media}'s website to download and use it. Make sure to credit the creator by showing the attribution information where you are sharing your work.", + "mediaDetails.reuse.copyrightDisclaimer": "Some photographs might contain copyrighted content, such as paintings, sculptures, or architectural works. Using these photographs may require additional permissions from the copyright holder of the depicted works.", + "mediaDetails.reuse.licenseHeader": "License", + "mediaDetails.reuse.toolHeader": "Public Domain", + "mediaDetails.reuse.audio": "Audio track", + "mediaDetails.reuse.image": "Image", + "mediaDetails.reuse.tool.content": "Read more about the tool {link}.", + /** + * Interpolated into mediaDetails.reuse.tool.content: + * _{link}_ part of "Read more about the tool {link}." + */ + "mediaDetails.reuse.tool.link": "here", + /** + * Interpolated into mediaDetails.reuse.credit.text: + * _{title}_ part of "_{title}_ by creator is licensed with CC0 1.0." + */ + "mediaDetails.reuse.credit.genericTitle": "This work", + "mediaDetails.reuse.credit.actualTitle": '"{title}"', + "mediaDetails.reuse.credit.text": "{title} {creator} {markedLicensed} {license}. {viewLegal}", + /** + * Interpolated into mediaDetails.reuse.credit.text: + * _{creator}_ part of 'This work _{creator}_ is licensed with CC0 1.0.' + */ + "mediaDetails.reuse.credit.creatorText": "by {creatorName}", + /** + * Interpolated into mediaDetails.reuse.credit.text: + * _{markedLicensed}_ part of "This work by creator _{markedLicensed}_ CC0 1.0." + */ + "mediaDetails.reuse.credit.marked": "is marked with", + /** + * Interpolated into mediaDetails.reuse.credit.text: + * _{markedLicensed}_ part of "This work by creator _{markedLicensed}_ CC BY 4.0." + */ + "mediaDetails.reuse.credit.licensed": "is licensed under", + "mediaDetails.reuse.credit.viewLegalText": "To view {termsCopy}, visit {url}.", + /** + * Interpolated into mediaDetails.reuse.credit.viewLegalText: + * _{termsCopy}_ part of "To view _{termsCopy}_, visit {url}." + */ + "mediaDetails.reuse.credit.termsText": "the terms", + /** + * Interpolated into mediaDetails.reuse.credit.viewLegalText: + * _{termsCopy}_ part of "To view {termsCopy}, visit {url}." + */ + "mediaDetails.reuse.credit.copyText": "a copy of this license", + "mediaDetails.reuse.copyLicense.title": "Credit the creator", + "mediaDetails.reuse.copyLicense.rich": "Rich Text", + "mediaDetails.reuse.copyLicense.html": "HTML", + "mediaDetails.reuse.copyLicense.plain": "Plain text", + "mediaDetails.reuse.copyLicense.copyText": "Copy text", + "mediaDetails.reuse.copyLicense.copied": "Copied!", + "mediaDetails.reuse.copyLicense.xml": "XML", + "mediaDetails.reuse.attribution": "This image was marked with a {link} license:", + "mediaDetails.providerLabel": "Provider", + "mediaDetails.sourceLabel": "Source", + "mediaDetails.providerDescription": "Website where the content is hosted", + "mediaDetails.sourceDescription": "Organization that created or owns the original content", + "mediaDetails.loading": "Loading...", + "mediaDetails.relatedError": "Error fetching related media", + "mediaDetails.aria.attribution.license": "read more about the license", + "mediaDetails.aria.attribution.tool": "read more about the tool", + "mediaDetails.aria.creatorUrl": "author {name}", + "mediaDetails.imageInfo": "Image information", + "mediaDetails.audioInfo": "Audio track information", + "mediaDetails.tags.title": "Tags", + "mediaDetails.tags.generated.heading": "Generated tags", + "mediaDetails.tags.generated.pageTitle": "Learn more", + "mediaDetails.tags.source.heading": "Source tags", + "mediaDetails.tags.showMore": "Show more", + "mediaDetails.tags.showLess": "Show less", + "mediaDetails.contentReport.short": "Report", + "mediaDetails.contentReport.long": "Report this content", + "mediaDetails.contentReport.form.disclaimer": "For security purposes, {openverse} collects and retains anonymized IP addresses of those who complete and submit this form.", + "mediaDetails.contentReport.form.question": "What is the reason?", + "mediaDetails.contentReport.form.dmca.option": "Infringes copyright", + "mediaDetails.contentReport.form.dmca.note": "You must fill out this {form} to report copyright infringement. No action will be taken until this form is filled out and submitted. We recommend doing the same at the source, {source}.", + /** + * Interpolated into mediaDetails.contentReport.dmca.note: + * _{form}_ part of "You must fill out this {form} to report copyright infringement." + */ + "mediaDetails.contentReport.form.dmca.form": "DMCA form", + "mediaDetails.contentReport.form.dmca.open": "Open form", + "mediaDetails.contentReport.form.sensitive.option": "Contains sensitive content", + "mediaDetails.contentReport.form.sensitive.subLabel": "Optional", + "mediaDetails.contentReport.form.sensitive.placeholder": "Optionally, provide a description.", + "mediaDetails.contentReport.form.other.option": "Other", + "mediaDetails.contentReport.form.other.note": "Describe the issue.", + "mediaDetails.contentReport.form.other.subLabel": "Required", + "mediaDetails.contentReport.form.other.placeholder": "Please enter at least 20 characters.", + "mediaDetails.contentReport.form.submit": "Report", + "mediaDetails.contentReport.form.cancel": "Cancel", + "mediaDetails.contentReport.success.title": "Report submitted successfully", + "mediaDetails.contentReport.success.note": "Thank you for reporting this content. We recommend doing the same at the source, {source}.", + "mediaDetails.contentReport.failure.title": "Report could not be submitted", + "mediaDetails.contentReport.failure.note": "Something went wrong, please try again after some time.", + "singleResult.back": "Back to results", + "imageDetails.creator": "by {name}", + "imageDetails.weblink": "Get this image", + "imageDetails.information.dimensions": "Dimensions", + "imageDetails.information.pixels": "pixels", + "imageDetails.information.sizeInPixels": "{width} × {height} pixels", + "imageDetails.relatedImages": "Related images", + "imageDetails.aria.creatorUrl": "Author {creator}", + "audioDetails.genreLabel": "Genre", + "audioDetails.relatedAudios": "Related audio tracks", + "audioDetails.table.album": "Album", + "audioDetails.table.sampleRate": "Sample Rate", + "audioDetails.table.filetype": "Format", + "audioDetails.table.genre": "Genre", + "audioDetails.weblink": "Get this audio track", + "allResults.snackbar.text": "Press {spacebar} to play or pause the track.", + /** + * Interpolated into allResults.snackbar.text: + * _{spacebar}_ part of "Press {spacebar} to play or pause the track." + */ + "allResults.snackbar.spacebar": "Spacebar", + "audioResults.snackbar.text": "Press {spacebar} to play or pause, and {left} & {right} to seek through the track.", + /** + * Interpolated into audioResults.snackbar.text: + * _{spacebar}_ part of 'Press {spacebar} to play or pause, and ← & → to seek through the track.' + */ + "audioResults.snackbar.spacebar": "Spacebar", + "audioResults.snackbar.left": "←", + "audioResults.snackbar.right": "→", + "externalSources.caption": "{openverse} can not guarantee the accuracy of license information. Always verify that the work is actually under a CC license.", + "externalSources.button": "Source list", + "externalSources.title": "External sources", + "externalSources.card.search": "Not finding what you're looking for? Try external sources", + "externalSources.card.caption": "Click on a source below to directly search other collections of CC-licensed images.{break}Please note that Use filters are not supported for Open Clip Art Library or Nappy.", + "externalSources.form.supportedTitle": "Not finding what you're looking for? Search in external sources", + "externalSources.form.supportedTitleSm": "Search in external sources", + "browsers.chrome": "Chrome", + "browsers.firefox": "Firefox", + "browsers.opera": "Opera", + "browsers.edge": "Edge", + "waveform.label": "Audio seek bar", + "waveform.currentTime": "{time} second|{time} seconds", + "audioThumbnail.alt": 'Cover art for "{title}" by {creator}', + "audioTrack.ariaLabel": "Audio: {title}", + "audioTrack.ariaLabelInteractive": "Audio: {title} - interactive player - press the space bar to play and pause a preview of the audio track", + "audioTrack.ariaLabelInteractiveSeekable": "Audio: {title} - interactive player - press the space bar to play and pause a preview of the audio track; use the left and right arrow keys to seek through the track.", + "audioTrack.messages.err_aborted": "You aborted playback.", + "audioTrack.messages.err_network": "A network error occurred.", + "audioTrack.messages.err_decode": "Could not decode audio track.", + "audioTrack.messages.err_unallowed": "Reproduction not allowed.", + "audioTrack.messages.err_unknown": "An unexpected error has occurred. Try again in a few minutes or report the item if the issue persists.", + "audioTrack.messages.err_unsupported": "This audio format is not supported by your browser.", + "audioTrack.messages.loading": "Loading...", + "audioTrack.creator": "by {creator}", + "audioTrack.close": "Close the audio player", + "playPause.play": "Play", + "playPause.pause": "Pause", + "playPause.replay": "Replay", + "playPause.loading": "Loading", + "search.search": "Search", + "search.searchBarLabel": "Search for content in {openverse}", + "licenseReadableNames.cc0": "Zero", + "licenseReadableNames.pdm": "Public Domain Mark", + "licenseReadableNames.by": "Attribution", + "licenseReadableNames.bySa": "Attribution-Share-Alike", + "licenseReadableNames.byNc": "Attribution-NonCommercial", + "licenseReadableNames.byNd": "Attribution-NoDerivatives", + "licenseReadableNames.byNcSa": "Attribution-NonCommercial-Share-Alike", + "licenseReadableNames.byNcNd": "Attribution-NonCommercial-NoDerivatives", + "licenseReadableNames.sampling+": "Sampling Plus", + "licenseReadableNames.ncSampling+": "NonCommercial Sampling Plus", interpunct: "•", - modal: { - close: "Close", - ariaClose: "Close the modal", - closeNamed: "Close {name}", - closeContentSettings: "Close the content settings menu", - closePagesMenu: "Close the pages menu", - closeBanner: "Close the banner", - }, - errorImages: { - depressedMusician: "A depressed pianist rests their head in their hands.", - waitingForABite: "Three boys sit on a broken log while two of them fish.", - }, - noResults: { - heading: 'We couldn\'t find anything for "{query}".', - alternatives: "Try a different query or use one of the external sources to expand your search.", - }, - serverTimeout: { - heading: "Whoops, it looks like that request took too long to complete. Please try again.", - }, - unknownError: { - heading: "Whoops, it looks like something went wrong. Please try again.", - }, - searchType: { - image: "Images", - audio: "Audio", - all: "All content", - video: "Videos", - model3d: "3D Models", - label: "Type of content to search", - heading: "Content type", - additional: "Coming soon", - statusBeta: "Beta", - seeImage: "See all images", - seeAudio: "See all audio tracks", - selectLabel: "Select a content type: {type}", - }, + "modal.close": "Close", + "modal.ariaClose": "Close the modal", + "modal.closeNamed": "Close {name}", + "modal.closeContentSettings": "Close the content settings menu", + "modal.closePagesMenu": "Close the pages menu", + "modal.closeBanner": "Close the banner", + "errorImages.depressedMusician": "A depressed pianist rests their head in their hands.", + "errorImages.waitingForABite": "Three boys sit on a broken log while two of them fish.", + "noResults.heading": 'We couldn\'t find anything for "{query}".', // codespell:ignore + "noResults.alternatives": "Try a different query or use one of the external sources to expand your search.", + "serverTimeout.heading": "Whoops, it looks like that request took too long to complete. Please try again.", + "unknownError.heading": "Whoops, it looks like something went wrong. Please try again.", + "searchType.image": "Images", + "searchType.audio": "Audio", + "searchType.all": "All content", + "searchType.video": "Videos", + "searchType.model3d": "3D Models", + "searchType.label": "Type of content to search", + "searchType.heading": "Content type", + "searchType.additional": "Coming soon", + "searchType.statusBeta": "Beta", + "searchType.seeImage": "See all images", + "searchType.seeAudio": "See all audio tracks", + "searchType.selectLabel": "Select a content type: {type}", skipToContent: "Skip to content", - prefPage: { - title: "Preferences", - groups: { - analytics: { - title: "Analytics", - desc: "{openverse} uses anonymous analytics to improve our service. We do not collect any information that can be used to identify you personally. However, if you would like not to participate, you can opt out here.", - }, - }, - features: { - analytics: "Record custom events and page views for analytics.", - }, - nonSwitchable: { - title: "Non-switchable features", - desc: "You cannot modify the status of these features.", - }, - switchable: { - title: "Switchable features", - desc: "You can toggle these features on or off as you like and your preferences will be saved in a cookie.", - }, - storeState: "Store state", - contentFiltering: "Content filtering", - explanation: "Shown because {featName} is {featState}", - }, + "prefPage.title": "Preferences", + "prefPage.groups.analytics.title": "Analytics", + "prefPage.groups.analytics.desc": "{openverse} uses anonymous analytics to improve our service. We do not collect any information that can be used to identify you personally. However, if you would like not to participate, you can opt out here.", + "prefPage.features.analytics": "Record custom events and page views for analytics.", + "prefPage.nonSwitchable.title": "Non-switchable features", + "prefPage.nonSwitchable.desc": "You cannot modify the status of these features.", + "prefPage.switchable.title": "Switchable features", + "prefPage.switchable.desc": "You can toggle these features on or off as you like and your preferences will be saved in a cookie.", + "prefPage.storeState": "Store state", + "prefPage.contentFiltering": "Content filtering", + "prefPage.explanation": "Shown because {featName} is {featState}", sketchfabIframeTitle: "{sketchfab} viewer", - flagStatus: { - nonexistent: "Nonexistent", - on: "On", - off: "Off", - }, - footer: { - wordpressAffiliation: "Part of the {wordpress} project", - wip: "🚧", - }, - language: { - language: "Language", - }, - theme: { - theme: "Theme", - choices: { - dark: "Dark", - light: "Light", - system: "System", - }, - }, - recentSearches: { - heading: "Recent searches", - clear: { - text: "Clear", - label: "Clear recent searches", - }, - clearSingle: { - label: "Clear recent search '{entry}'", - }, - none: "No recent searches to show.", - disclaimer: "Recent searches are saved only on your device.", - }, - report: { - imageDetails: "See image details", - }, - sensitiveContent: { - title: { - image: "This image may contain sensitive content.", - audio: "This audio track may contain sensitive content.", - }, - creator: "Creator", - singleResult: { - title: "Sensitive content", - hide: "Hide content", - show: "Show content", - explanation: "This work is marked as sensitive for the following reasons:", - learnMore: "{link} about how {openverse} handles sensitive content.", - /** - * Interpolated into sensitiveContent.singleResult.learnMore: - * _{link}_ part of "_{link}_ about how Openverse handles sensitive content." - */ - link: "Learn more", - }, - reasons: { - providerSuppliedSensitive: "The source of this work has marked it as sensitive.", - sensitiveText: "{openverse} has detected potentially sensitive text.", - userReportedSensitive: "{openverse} users have reported this work as sensitive.", - }, - }, - collection: { - heading: { - tag: "Tag", - creator: "Creator", - source: "Source", - }, - pageTitle: { - tag: { - /** - * This will be the page title and must be SEO friendly. - * {tag} will be a dynamic value such as "cat". We cannot change its case or form. - * You can change the sentence to add more context and make the sentence - * grammatically correct, for instance, to "Audio tracks with the tag {tag}". - */ - audio: "{tag} audio tracks", - - /** - * This will be the page title and must be SEO friendly. - * {tag} will be a dynamic value such as "cat". We cannot change its case or form. - * You can change the sentence to add more context and make the sentence - * grammatically correct, for instance, to "Images with the tag {tag}". - */ - image: "{tag} images", - }, - source: { - /** - * This will be the page title and must be SEO friendly. - * {source} will be a dynamic value such as "Wikimedia". We cannot change its case or form. - * You can change the sentence to add more context and make the sentence - * grammatically correct, for instance, to "Audio tracks from {source}". - */ - audio: "{source} audio tracks", - /** - * This will be the page title and must be SEO friendly. - * {source} will be a dynamic value such as "Wikimedia". We cannot change its case or form. - * You can change the sentence to add more context and make the sentence - * grammatically correct, for instance, to "Images from {source}". - */ - image: "{source} images", - }, - }, - link: { - source: "Open source site", - creator: "Open creator page", - }, - ariaLabel: { - creator: { - audio: "Audio tracks by {creator} in {source}", - image: "Images by {creator} in {source}", - }, - source: { - audio: "Audio tracks from {source}", - image: "Images from {source}", - }, - tag: { - audio: "Audio tracks with the tag {tag}", - image: "Images with the tag {tag}", - }, - }, - resultCountLabel: { - creator: { - audio: { - zero: "No audio tracks by this creator.", - count: "{count} audio track by this creator.|{count} audio tracks by this creator.", - countMore: "Top {count} audio tracks by this creator.", - }, - image: { - zero: "No images by this creator.", - count: "{count} image by this creator.|{count} images by this creator.", - countMore: "Top {count} images by this creator.", - }, - }, - source: { - audio: { - zero: "No audio tracks provided by this source", - count: "{count} audio track provided by this source|{count} audio tracks provided by this source", - countMore: "Top {count} audio tracks provided by this source", - }, - image: { - zero: "No images provided by this source", - count: "{count} image provided by this source|{count} images provided by this source", - countMore: "Top {count} images provided by this source", - }, - }, - tag: { - audio: { - zero: "No audio tracks with the selected tag", - count: "{count} audio track with the selected tag|{count} audio tracks with the selected tag", - countMore: "Top {count} audio tracks with the selected tag", - }, - image: { - zero: "No images with the selected tag", - count: "{count} image with the selected tag|{count} images with the selected tag", - countMore: "Top {count} images with the selected tag", - }, - }, - }, - }, + "flagStatus.nonexistent": "Nonexistent", + "flagStatus.on": "On", + "flagStatus.off": "Off", + "footer.wordpressAffiliation": "Part of the {wordpress} project", + "footer.wip": "🚧", + "language.language": "Language", + "theme.theme": "Theme", + "theme.choices.dark": "Dark", + "theme.choices.light": "Light", + "theme.choices.system": "System", + "recentSearches.heading": "Recent searches", + "recentSearches.clear.text": "Clear", + "recentSearches.clear.label": "Clear recent searches", + "recentSearches.clearSingle.label": "Clear recent search '{entry}'", + "recentSearches.none": "No recent searches to show.", + "recentSearches.disclaimer": "Recent searches are saved only on your device.", + "report.imageDetails": "See image details", + "sensitiveContent.title.image": "This image may contain sensitive content.", + "sensitiveContent.title.audio": "This audio track may contain sensitive content.", + "sensitiveContent.creator": "Creator", + "sensitiveContent.singleResult.title": "Sensitive content", + "sensitiveContent.singleResult.hide": "Hide content", + "sensitiveContent.singleResult.show": "Show content", + "sensitiveContent.singleResult.explanation": "This work is marked as sensitive for the following reasons:", + "sensitiveContent.singleResult.learnMore": "{link} about how {openverse} handles sensitive content.", + /** + * Interpolated into sensitiveContent.singleResult.learnMore: + * _{link}_ part of '_{link}_ about how Openverse handles sensitive content.' + */ + "sensitiveContent.singleResult.link": "Learn more", + "sensitiveContent.reasons.providerSuppliedSensitive": "The source of this work has marked it as sensitive.", + "sensitiveContent.reasons.sensitiveText": "{openverse} has detected potentially sensitive text.", + "sensitiveContent.reasons.userReportedSensitive": "{openverse} users have reported this work as sensitive.", + "collection.heading.tag": "Tag", + "collection.heading.creator": "Creator", + "collection.heading.source": "Source", + /** + * This will be the page title and must be SEO friendly. + * {tag} will be a dynamic value such as "cat". We cannot change its case or form. + * You can change the sentence to add more context and make the sentence + * grammatically correct, for instance, to 'Audio tracks with the tag {tag}'. + */ + "collection.pageTitle.tag.audio": "{tag} audio tracks", + /** + * This will be the page title and must be SEO friendly. + * {tag} will be a dynamic value such as "cat". We cannot change its case or form. + * You can change the sentence to add more context and make the sentence + * grammatically correct, for instance, to 'Images with the tag {tag}'. + */ + "collection.pageTitle.tag.image": "{tag} images", + /** + * This will be the page title and must be SEO friendly. + * {source} will be a dynamic value such as 'Wikimedia'. We cannot change its case or form. + * You can change the sentence to add more context and make the sentence + * grammatically correct, for instance, to 'Audio tracks from {source}'. + */ + "collection.pageTitle.source.audio": "{source} audio tracks", + /** + * This will be the page title and must be SEO friendly. + * {source} will be a dynamic value such as 'Wikimedia'. We cannot change its case or form. + * You can change the sentence to add more context and make the sentence + * grammatically correct, for instance, to "Images from {source}". + */ + "collection.pageTitle.source.image": "{source} images", + "collection.link.source": "Open source site", + "collection.link.creator": "Open creator page", + "collection.ariaLabel.creator.audio": "Audio tracks by {creator} in {source}", + "collection.ariaLabel.creator.image": "Images by {creator} in {source}", + "collection.ariaLabel.source.audio": "Audio tracks from {source}", + "collection.ariaLabel.source.image": "Images from {source}", + "collection.ariaLabel.tag.audio": "Audio tracks with the tag {tag}", + "collection.ariaLabel.tag.image": "Images with the tag {tag}", + "collection.resultCountLabel.creator.audio.zero": "No audio tracks by this creator.", + "collection.resultCountLabel.creator.audio.count": "{count} audio track by this creator.|{count} audio tracks by this creator.", + "collection.resultCountLabel.creator.audio.countMore": "Top {count} audio tracks by this creator.", + "collection.resultCountLabel.creator.image.zero": "No images by this creator.", + "collection.resultCountLabel.creator.image.count": "{count} image by this creator.|{count} images by this creator.", + "collection.resultCountLabel.creator.image.countMore": "Top {count} images by this creator.", + "collection.resultCountLabel.source.audio.zero": "No audio tracks provided by this source", + "collection.resultCountLabel.source.audio.count": "{count} audio track provided by this source|{count} audio tracks provided by this source", + "collection.resultCountLabel.source.audio.countMore": "Top {count} audio tracks provided by this source", + "collection.resultCountLabel.source.image.zero": "No images provided by this source", + "collection.resultCountLabel.source.image.count": "{count} image provided by this source|{count} images provided by this source", + "collection.resultCountLabel.source.image.countMore": "Top {count} images provided by this source", + "collection.resultCountLabel.tag.audio.zero": "No audio tracks with the selected tag", + "collection.resultCountLabel.tag.audio.count": "{count} audio track with the selected tag|{count} audio tracks with the selected tag", + "collection.resultCountLabel.tag.audio.countMore": "Top {count} audio tracks with the selected tag", + "collection.resultCountLabel.tag.image.zero": "No images with the selected tag", + "collection.resultCountLabel.tag.image.count": "{count} image with the selected tag|{count} images with the selected tag", + "collection.resultCountLabel.tag.image.countMore": "Top {count} images with the selected tag", } diff --git a/frontend/i18n/locales/README.md b/frontend/i18n/locales/README.md deleted file mode 100644 index ae613e25bfb..00000000000 --- a/frontend/i18n/locales/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Locales - -The primary internationalisation file is [`data/en.json5`](../data/en.json5). -All `.json` files present in this directory are re-generated when updating -translations, so they should not be modified. diff --git a/frontend/i18n/locales/scripts/README.md b/frontend/i18n/locales/scripts/README.md deleted file mode 100644 index fbe3ecb8c43..00000000000 --- a/frontend/i18n/locales/scripts/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Locale scripts - -Locale scripts should be run in the root of the repository using their -respective pnpm commands, in the `i18n` namespace. diff --git a/frontend/i18n/locales/scripts/axios.js b/frontend/i18n/locales/scripts/axios.js deleted file mode 100644 index 65a12621a1a..00000000000 --- a/frontend/i18n/locales/scripts/axios.js +++ /dev/null @@ -1,7 +0,0 @@ -const axios = require("axios") - -const { userAgent } = require("../../../shared/constants/user-agent") - -module.exports = module.exports = axios.create({ - headers: { "User-Agent": userAgent }, -}) diff --git a/frontend/i18n/locales/scripts/bulk-download.js b/frontend/i18n/locales/scripts/bulk-download.js deleted file mode 100644 index fa70fe94a0a..00000000000 --- a/frontend/i18n/locales/scripts/bulk-download.js +++ /dev/null @@ -1,86 +0,0 @@ -const { pipeline } = require("stream/promises") -const { createWriteStream } = require("fs") - -const AdmZip = require("adm-zip") - -const { writeLocaleFile } = require("./utils") -const axios = require("./axios") -const jed1xJsonToJson = require("./jed1x-json-to-json") - -const BULK_DOWNLOAD_URL = - "https://translate.wordpress.org/exporter/meta/openverse/-do/" - -/** - * Fetch the ZIP of translations strings from GlotPress using the authentication - * cookies to access the page. - * - * @return {Promise}} - the path to the downloaded ZIP file - */ -const fetchBulkJed1x = async () => { - const res = await axios.get(BULK_DOWNLOAD_URL, { - params: { "export-format": "jed1x" }, - responseType: "stream", - }) - const destPath = process.cwd() + "/i18n/locales/openverse.zip" - await pipeline(res.data, createWriteStream(destPath)) - return destPath -} - -/** - * Extract all JSON file from the given ZIP file. Their names are sanitised to - * be in the format `.json`. - * - * TODO: Remove deprecated keys handling once all po keys are updated. - * https://github.com/WordPress/openverse/issues/2438 - * - * @param zipPath {string} - the path to the ZIP file to extract - * @return {Promise} - the outcome of writing all ZIP files - */ -const extractZip = async (zipPath) => { - const zip = new AdmZip(zipPath, undefined) - const localeJsonMap = zip - .getEntries() - .filter((entry) => entry.entryName.endsWith(".json")) - .map((entry) => { - const jed1xObj = JSON.parse(zip.readAsText(entry)) - const vueI18nObj = jed1xJsonToJson(jed1xObj) - const localeName = entry.name - .replace("meta-openverse-", "") - .replace(".jed.json", "") - return [localeName, vueI18nObj] - }) - const deprecatedKeys = { count: 0, keys: {} } - - const result = await Promise.all( - localeJsonMap.map((args) => writeLocaleFile(...args, deprecatedKeys)) - ) - const issue = "https://github.com/WordPress/openverse/issues/2438" - if (deprecatedKeys.count > 0) { - let warning = `${deprecatedKeys.count} deprecated kebab-case keys replaced in locale files. ` - warning += `To see the keys, run \`ov just frontend/run i18n:get-translations --verbose\`. For more details, see ${issue}.` - if (process.argv.includes("--verbose")) { - warning += `\n${JSON.stringify(deprecatedKeys.keys, null, 2)}` - } - console.warn(warning) - } else { - console.log( - `No deprecated kebab-case keys found in locale files. 🎉 Please close issue ${issue}.` - ) - } - return result -} - -/** - * Perform a bulk download of translation strings from GlotPress and extract the - * JSON files from the ZIP archive. - * - * @return {Promise} - whether the bulk download succeeded - */ -const bulkDownload = async () => { - console.log("Performing bulk download.") - const zipPath = await fetchBulkJed1x() - const translations = await extractZip(zipPath) - console.log(`Successfully saved ${translations.length} translations.`) -} - -module.exports = bulkDownload diff --git a/frontend/i18n/locales/scripts/create-wp-locale-list.js b/frontend/i18n/locales/scripts/create-wp-locale-list.js deleted file mode 100644 index 32a2426854f..00000000000 --- a/frontend/i18n/locales/scripts/create-wp-locale-list.js +++ /dev/null @@ -1,120 +0,0 @@ -/** -This script extracts data for locales available in GlotPress and translate.wp.org, - transforms some locale properties to match what Vue i18n expects, - and saves it to `wp-locales-list.json`. - **/ - -const fs = require("fs") - -const axios = require("./axios") -const { addFetchedTranslationStatus } = require("./get-translations-status") - -const base_url = - "https://raw.githubusercontent.com/GlotPress/GlotPress-WP/develop/locales/locales.php" - -/** - * Fetches the data from GlotPress GitHub. - * @returns {Promise} - */ -async function getGpLocalesData() { - const res = await axios.get(base_url) - return res.data -} - -const snakeToCamel = (str) => - str - .toLowerCase() - .replace(/([-_][a-z])/g, (group) => - group.toUpperCase().replace("-", "").replace("_", "") - ) - -const createPropertyRePatterns = ({ - properties = [ - "english_name", - "native_name", - "lang_code_iso_639_1", // used for HTML lang attribute - "lang_code_iso_639_2", // used for HTML lang attribute, fallback from previous - "lang_code_iso_639_3", // used for HTML lang attribute, fallback from previous - "slug", // unique identifier used by Nuxt i18n - "text_direction", - ], -} = {}) => { - const propertyRePatterns = {} - properties.forEach((prop) => { - propertyRePatterns[prop] = new RegExp(`${prop} *= *['](.*)['];`) - }) - return propertyRePatterns -} - -function parseLocaleData(rawData) { - const wpLocalePattern = /wp_locale *= *'(.*)';/ - const propertyRePatterns = createPropertyRePatterns() - const wpLocaleMatch = rawData.match(wpLocalePattern) - - // ugly check to exclude English from the locales list, - // so we don't overwrite `en.json` later. See `get-translations.js` - // to check how `en.json` file is created. - if (wpLocaleMatch && wpLocaleMatch[1] !== "en_US") { - const wpLocale = wpLocaleMatch[1] - const data = {} - - Object.keys(propertyRePatterns).forEach((key) => { - const pattern = propertyRePatterns[key] - const value = rawData.match(pattern) - if (value) { - // Convert locale property names to camelCase and replace `english_name` with `name` - const camelCasedPropName = snakeToCamel( - key === "english_name" ? "name" : key - ) - data[camelCasedPropName] = value[1] - } - }) - - return [wpLocale, data] - } -} - -/** - * Fetches locale data from the GP GitHub. - * Extracts properties and converts locale property names to camelCase as expected - * by Vue i18n. - * Fetches data from translate.wordpress.org, leaves only the locales available - * there, and adds `code` and `translated` properties to each locale. - * @returns {Promise<{}>} - */ -async function getWpLocaleData() { - const data = await getGpLocalesData() - const rawLocalesData = data - .split("new GP_Locale();") - .splice(1) - .map((item) => item.trim()) - - const locales = Object.fromEntries( - rawLocalesData.map(parseLocaleData).filter(Boolean) - ) - console.log(`${rawLocalesData.length} locales found in GP source code.`) - - const unsortedLocales = await addFetchedTranslationStatus(locales) - console.log( - `${Object.keys(unsortedLocales).length} locales found in WP GP instance.` - ) - - return Object.keys(unsortedLocales) - .sort() - .reduce((accumulator, currentValue) => { - accumulator[currentValue] = unsortedLocales[currentValue] - return accumulator - }, {}) -} - -getWpLocaleData() - .then((data) => { - try { - const fileName = process.cwd() + "/i18n/locales/scripts/wp-locales.json" - fs.writeFileSync(fileName, JSON.stringify(data, null, 2) + "\n") - console.log(`Successfully wrote locales list file to ${fileName}`) - } catch (err) { - console.error(err) - } - }) - .catch((err) => console.log("Could not fetch data from ", base_url, err)) diff --git a/frontend/i18n/locales/scripts/get-translations-status.js b/frontend/i18n/locales/scripts/get-translations-status.js deleted file mode 100644 index cc608f91281..00000000000 --- a/frontend/i18n/locales/scripts/get-translations-status.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Fetch the list of locales that are available on translate.wordpress.org - * and the translation status for all of them. - * Update the GP locales object with this data, and removes any of the GP - * locales that are not available on translate.wordpress.org. - */ -const parser = require("node-html-parser") - -const axios = require("./axios") - -const baseUrl = "https://translate.wordpress.org/projects/meta/openverse/" - -function parseRow(row) { - const cells = row.querySelectorAll("td") - const langName = cells[0].querySelector("a").text.trim() - const percentTranslated = parseInt(cells[1].text.trim().replace("%", ""), 10) - return [langName, percentTranslated] -} - -/** - * Takes an object with all gpLocales, and filters it to return only the locales - * available at translate.wordpress.org. Also, adds the `code` (the same as GlotPress - * `slug`), and `translated` with the percentage of translated strings, to each - * locale object. - */ -const addFetchedTranslationStatus = async (gpLocales) => { - const raw = await axios.get(baseUrl) - const parsed = parser.parse(raw.data) - const langPercent = Object.fromEntries( - parsed.querySelector("tbody").querySelectorAll("tr").map(parseRow) - ) - - return Object.fromEntries( - Object.entries(gpLocales) - .filter(([, langObject]) => - Object.hasOwnProperty.apply(langPercent, [langObject.name]) - ) - .map(([langKey, langObject]) => [ - langKey, - { - ...langObject, - translated: langPercent[langObject.name], - }, - ]) - ) -} - -module.exports = { addFetchedTranslationStatus } diff --git a/frontend/i18n/locales/scripts/get-translations.js b/frontend/i18n/locales/scripts/get-translations.js deleted file mode 100644 index c2519114b25..00000000000 --- a/frontend/i18n/locales/scripts/get-translations.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Fetch the NGX-Translate JSON file for each supported language, - * convert to our JSON format, and save in the correct folder. - */ - -const { writeFileSync, existsSync } = require("fs") -const os = require("os") - -const chokidar = require("chokidar") - -const { parseJson } = require("./read-i18n") - -/** - * Write `en.json` from `en.json5`. - */ -const writeEnglish = () => { - const rootEntry = parseJson("en.json5") - writeFileSync( - process.cwd() + "/i18n/locales/en.json", - JSON.stringify(rootEntry, null, 2) + os.EOL - ) - console.log("Successfully saved English translation to en.json.") -} - -writeEnglish() -if (process.argv.includes("--watch")) { - console.log("Watching en.json5 for changes...") - chokidar - .watch(process.cwd() + "/i18n/data/en.json5") - .on("all", (event, path) => { - console.log(`Event '${event}' for file ${path}`) - writeEnglish() - }) -} - -if (!process.argv.includes("--en-only")) { - const bulkDownload = require("./bulk-download") - - bulkDownload().catch((err) => { - console.error(err) - console.error(":'-( Downloading translations failed.") - if (process.argv.includes("--require-complete")) { - process.exitCode = 1 - } - }) -} else { - // Create valid-locales.json if it doesn't exist. It is required for Nuxt to build the app. - const validLocalesFilePath = - process.cwd() + "/i18n/locales/scripts/valid-locales.json" - if (!existsSync(validLocalesFilePath)) { - writeFileSync(validLocalesFilePath, "[]") - console.log("Created empty valid-locales.json.") - } -} diff --git a/frontend/i18n/locales/scripts/get-validated-locales.js b/frontend/i18n/locales/scripts/get-validated-locales.js deleted file mode 100644 index ec78f100e3d..00000000000 --- a/frontend/i18n/locales/scripts/get-validated-locales.js +++ /dev/null @@ -1,86 +0,0 @@ -/* -Updates the translation status of locales list from wp-locales-list, -and saves lists of translated and untranslated locales with properties expected -by Vue i18n. - */ -const fs = require("fs") - -const localesList = require("./wp-locales.json") -const { addFetchedTranslationStatus } = require("./get-translations-status") - -/** - * Returns a list of locale objects with at least one translated string - * @returns {{ - * translated: import('./types').I18nLocaleProps[], - * untranslated: import('./types').I18nLocaleProps[] - * invalid: import('./types').I18nLocaleProps[], - * }} - */ -const getValidatedLocales = async () => { - const result = { - translated: [], - untranslated: [], - invalid: [], - } - const updatedLocaleList = await addFetchedTranslationStatus(localesList) - const allLocales = Object.values(updatedLocaleList).map((locale) => ({ - /* Nuxt i18n fields */ - - code: locale.slug, - dir: locale.textDirection || "ltr", - file: `${locale.slug}.json`, - // Used for the html lang attribute. - language: locale.slug, - - /* Custom fields */ - - name: locale.name, - nativeName: locale.nativeName || locale.name, - translated: locale.translated, - })) - for (const locale of allLocales) { - const fileLocation = `${process.cwd()}/i18n/locales/${locale.file}` - if (fs.existsSync(fileLocation)) { - if (Object.keys(JSON.parse(fs.readFileSync(fileLocation))).length) { - result.translated.push(locale) - } else { - result.untranslated.push(locale) - } - } else { - result.invalid.push(locale) - } - } - return result -} - -try { - getValidatedLocales().then((locales) => { - console.log(`Found ${locales.translated.length} locales with translations.`) - const fileName = "valid-locales.json" - const valid = locales.translated - fs.writeFileSync( - process.cwd() + `/i18n/locales/scripts/` + fileName, - JSON.stringify(valid, null, 2) + "\n" - ) - - console.log( - `Found ${locales.untranslated.length} locales without translations.` - ) - const untranslatedFileName = "untranslated-locales.json" - fs.writeFileSync( - process.cwd() + `/i18n/locales/scripts/` + untranslatedFileName, - JSON.stringify(locales.untranslated, null, 2) + "\n" - ) - - console.log(`Found ${locales.invalid.length} invalid locales.`) - const invalidFileName = "invalid-locales.json" - fs.writeFileSync( - process.cwd() + `/i18n/locales/scripts/` + invalidFileName, - JSON.stringify(locales.invalid, null, 2) + "\n" - ) - - console.log(`> Wrote locale metadata for @nuxt/i18n.`) - }) -} catch (err) { - console.error(err) -} diff --git a/frontend/i18n/locales/scripts/jed1x-json-to-json.js b/frontend/i18n/locales/scripts/jed1x-json-to-json.js deleted file mode 100644 index 92d1f10201b..00000000000 --- a/frontend/i18n/locales/scripts/jed1x-json-to-json.js +++ /dev/null @@ -1,69 +0,0 @@ -const { setToValue } = require("./utils") - -/** - * Convert a Jed1x-Translate object to a nested JSON object. - * - * Create a nested JSON object, clean up keys from jed1x context with special - * character, remove keys without values, convert values from array to string, - * if strings are for plural forms, join them with the pipe character. - * Go from this: - * { - * "browsePage.load\u0004Load more results": [ - * "Загрузить ещё результаты" - * ], - * "browsePage.allResultCountMore\u0004Over ###localeCount### results": [ - * "Более ###localeCount### результата", - * "Более ###localeCount### результатов", - * "Более ###localeCount### результатов" - * ] - * "browsePage.searchForm.button\u0004Search": [], - * } - * To: - * { - * "browsePage: { - * "load": "Загрузить ещё результаты" - * "all-result-count-more": "Более ###localeCount### результата|Более ###localeCount### результатов|Более ###localeCount### результатов", - * } - * - */ - -// special character, context delimiter in jed format: -// https://github.com/messageformat/Jed/blob/351c47d5c57c5c81e418414c53ca84075c518edb/jed.js#L117 -const SPLIT_CHAR = String.fromCharCode(4) - -function jed1xJsonToJson(jed1xObject) { - const result = {} - Object.entries(jed1xObject?.locale_data?.messages).forEach(([key, value]) => { - const cleanedKey = key.slice(0, key.indexOf(SPLIT_CHAR)) - if (value.length > 0) { - const cleanedValue = value.length === 1 ? value[0] : value.join("|") - return setToValue(result, cleanedKey, cleanedValue) - } - }) - return result -} - -// test -// node jed1x-json-to-json.js -// console.log( -// JSON.stringify( -// jed1xJsonToJson({ -// locale_data: { -// messages: { -// 'browse-page.load\u0004Load more results': [ -// 'Загрузить ещё результаты', -// ], -// 'browse-page.all-result-count-more\u0004Over ###localeCount### results': -// [ -// 'Более ###localeCount### результата', -// 'Более ###localeCount### результатов', -// 'Более ###localeCount### результатов', -// ], -// 'browse-page.search-form.button\u0004Search': [], -// }, -// }, -// }) -// ) -// ) - -module.exports = jed1xJsonToJson diff --git a/frontend/i18n/locales/scripts/json-helpers.js b/frontend/i18n/locales/scripts/json-helpers.js deleted file mode 100644 index 7f8ba2703ab..00000000000 --- a/frontend/i18n/locales/scripts/json-helpers.js +++ /dev/null @@ -1,64 +0,0 @@ -// Copied from these two libraries: -// https://github.com/coderaiser/all-object-keys -// https://github.com/coderaiser/jessy -const isObject = (a) => typeof a === "object" -const isEmptyObject = (a) => !Object.keys(a).length -const isSimple = (a) => !a || !isObject(a) || isEmptyObject(a) -const pop = (a) => a.pop() || [] - -function getAllPaths(obj) { - const result = [] - const [currentResult, stack] = readPaths(obj) - result.push(...currentResult) - let [key, current] = pop(stack) - while (current) { - const [currentResult, currentStack] = readPaths(current, key) - result.push(...currentResult) - stack.push(...currentStack) - // [key, current] = pop(stack) - doesn't work, cannot create property '[object Object]' - const values = pop(stack) - key = values[0] - current = values[1] - } - return result -} - -const { entries } = Object - -function readPaths(obj, path = "") { - const divider = "." - const result = [] - const stack = [] - for (const [key, value] of entries(obj)) { - const fullPath = !path ? key : `${path}${divider}${key}` - if (isSimple(value)) { - result.push(fullPath) - continue - } - if (value === obj) { - continue - } - stack.push([fullPath, value]) - } - return [result, stack] -} -const getKeyValue = (key, value) => { - const selects = key.split(".") - selects.some((name, i) => { - const nestedName = selects.slice(i).join(".") - if (typeof value[nestedName] !== "undefined") { - value = value[nestedName] - return true - } - if (!value[name]) { - value = undefined - return true - } - value = value[name] - - return !value - }) - - return value -} -module.exports = { getAllPaths, getKeyValue } diff --git a/frontend/i18n/locales/scripts/json-pot-helpers.js b/frontend/i18n/locales/scripts/json-pot-helpers.js deleted file mode 100644 index bfa9b0f6a4d..00000000000 --- a/frontend/i18n/locales/scripts/json-pot-helpers.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * More about the structure of .po files: - * // https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files - * - * ```po - * white-space - * # translator-comments - * #. extracted-comments - * #: reference… - * #, flag… - * #| msgid previous-untranslated-string - * msgid untranslated-string - * msgstr translated-string - * ``` - */ - -const { getParsedVueFiles } = require("./parse-vue-files") - -const PARSED_VUE_FILES = getParsedVueFiles("**/*.?(js|vue)") - -/** @param str {string} */ -const escapeQuotes = (str) => str.replace(/"/g, '\\"') - -/** @param str {string} */ -const containsCurlyWord = (str) => /\{[a-zA-Z-]*}/.test(str) - -/** @param str {string} */ -const checkStringForVars = (str) => - containsCurlyWord(str) ? "#. Do not translate words between ### ###." : "" - -/** - * For GlotPress to display warning when the translators miss the placeholders - * or try replacing them with something else, we need to surround the - * placeholders with `###`. - * - * @param str {string} the translation string - * @return {string} the translation string with all placeholders marked - */ -const replaceVarsPlaceholders = (str) => { - if (!containsCurlyWord(str)) { - return str - } - - const variable = /\{(?[a-zA-Z-]*)}/g - return str.replace(variable, `###$###`) -} - -/** - * Replace placeholder format for variables and escape quotes. - * - * @param str {string} the translation string - * @return {string} the translation string with quotes escaped and placeholders marked - */ -const processValue = (str) => escapeQuotes(replaceVarsPlaceholders(str)) - -/** - * Returns a comment with all reference to the file and line where the string is - * used. These are prefixed with `#:`. - * - * @param keyPath {string} the lineage of the entry to search in Vue files - * @return {string[]} the list of reference comments - */ -const getRefComments = (keyPath) => - PARSED_VUE_FILES.filter((k) => k.path === keyPath).map( - (item) => `#: ${item.file}:${item.line}` - ) - -const pot_creation_date = () => - `${new Date().toISOString().split(".")[0]}+00:00` - -const POT_FILE_META = `# Copyright (C) 2021 -# This file is distributed under the same license as Openverse. -msgid "" -msgstr "" -"Project-Id-Version: Openverse \\n" -"Report-Msgid-Bugs-To: https://github.com/wordpress/openverse/issues \\n" -"POT-Creation-Date: ${pot_creation_date()}\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"PO-Revision-Date: 2021-MO-DA HO:MI+ZONE\\n" -"Last-Translator: FULL NAME \\n" -"Language-Team: LANGUAGE \\n" -` - -/** - * Generate the comment for the POT entry. This includes any comment written on - * the JSON entry, a message about `###` and finally references to where that - * entry is used in the codebase. - * - * @param entry {import('./read-i18n').Entry} the entry to get the comment for - * @return {string} the comment lines - */ -const getComment = (entry) => { - const comment = [] - - // comments given by the programmer, directed at the translator (#.) - if (entry.doc) { - comment.push(`#. ${entry.doc}`) - } - - // comments given by the programmer, directed at the translator (#.) - const vars = checkStringForVars(entry.value) - if (vars) { - comment.push(vars) - } - - // comments containing references to the program’s source code (#:) - const refComments = getRefComments(entry.lineage) - if (refComments.length) { - comment.push(...refComments) - } - - return comment.map((item) => `${item}`).join("\n") -} - -/** - * Convert a JSON entry into a POT entry. If the JSON entry has nested entries, - * recursively convert them as well. - * - * @param entry {import('./read-i18n').Entry} the entry to convert to POT - * @return {string} the POT equivalent of the JSON entry - */ -const toPot = (entry) => { - if (!entry.value) { - // string-object type mapping - return entry.children.map((child) => toPot(child)).join("\n\n") - } - - // string-string type mapping - const poEntry = [] - const comment = getComment(entry) - if (comment) { - poEntry.push(comment) - } - poEntry.push(`msgctxt "${entry.lineage}"`) - if (entry.value.includes("|") && /(count|time)/i.test(entry.value)) { - const pluralizedValues = entry.value.split("|") - if (pluralizedValues.length === 1) { - pluralizedValues.push(pluralizedValues[0]) - } - poEntry.push( - `msgid "${processValue(pluralizedValues[0])}"`, - `msgid_plural "${processValue(pluralizedValues[1])}"`, - 'msgstr[0] ""', - 'msgstr[1] ""' - ) - } else { - poEntry.push(`msgid "${processValue(entry.value)}"`, 'msgstr ""') - } - return poEntry.join("\n") -} - -/** - * Given the root entry generated by `read-i18n.parseJson`, this function - * returns the complete text to output to the Openverse POT file. - * - * @param entry {import('./read-i18n').Entry} the root entry of the JSON file - * @return {string} the text content of the Openverse POT file - */ -const makePot = (entry) => [POT_FILE_META, toPot(entry)].join("\n") - -module.exports = { replaceVarsPlaceholders, makePot } diff --git a/frontend/i18n/locales/scripts/json-to-pot.js b/frontend/i18n/locales/scripts/json-to-pot.js deleted file mode 100644 index 9f5a65339dc..00000000000 --- a/frontend/i18n/locales/scripts/json-to-pot.js +++ /dev/null @@ -1,15 +0,0 @@ -const fs = require("fs") -const path = require("path") - -const { parseJson } = require("./read-i18n") -const { makePot } = require("./json-pot-helpers") - -try { - const fileName = path.resolve(process.cwd(), "openverse.pot") - - const entries = parseJson("en.json5") - fs.writeFileSync(fileName, makePot(entries), { flag: "w" }) - console.log(`Successfully wrote pot file to ${fileName}`) -} catch (err) { - console.error(err) -} diff --git a/frontend/i18n/locales/scripts/ngx-json-to-json.js b/frontend/i18n/locales/scripts/ngx-json-to-json.js deleted file mode 100644 index d489d3d6e77..00000000000 --- a/frontend/i18n/locales/scripts/ngx-json-to-json.js +++ /dev/null @@ -1,47 +0,0 @@ -const { setToValue } = require("./utils") - -/** - * Convert an NGX-Translate object to a nested JSON object - * - * Go from this: - * { - * "photo-details.aria.share.pinterest": "compartir en pinterest", - * "photo-details.aria.share.twitter": "compartir en twitter", - * "photo-details.aria.share.facebook": "compartir en facebook", - * } - * To: - * { - * "photo-details": { - * "aria": { - * "share": { - * "twitter": "compartir en pinterest", - * "facebook": "compartir en twitter", - * "pinterest": "compartir en facebook", - * } - * } - * } - * } - * - */ -function ngxJsonToJson(ngxObject) { - const result = {} - Object.entries(ngxObject).forEach( - ([key, value]) => value && setToValue(result, key, value) - ) - return result -} - -// test -// node ngx-json-to-json.js -// console.log( -// JSON.stringify( -// ngxJsonToJson({ -// 'wow.okay.cool': null, -// 'photo-details.aria.share.pinterest': 'compartir en pinterest', -// 'photo-details.aria.share.twitter': 'compartir en twitter', -// 'photo-details.aria.share.facebook': 'compartir en facebook', -// }) -// ) -// ) - -module.exports = ngxJsonToJson diff --git a/frontend/i18n/locales/scripts/read-i18n.js b/frontend/i18n/locales/scripts/read-i18n.js deleted file mode 100644 index 6e426c8919d..00000000000 --- a/frontend/i18n/locales/scripts/read-i18n.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * In `en.json` we use a limited version of JSON5. It is valid JSON5 but only - * uses a limited set of features and syntax. This package provides functions - * to read this format and convert it into regular JSON. - * - * The JSON5 file is converted to an AST by Babel, treating it as a JavaScript - * `ObjectExpression` which is then converted into an `Entry` instance, which - * is capable of emitting regular JSON. - * - * @typedef {{ [key: string]: string | SimJson }} SimJson - */ - -const fs = require("fs") -const path = require("path") - -const babel = require("@babel/parser") - -/** - * An `Entry` refers to one i18n translation definition. It can be one of two - * types: - * - string-string: where the value of the key is one string (e.g. 'key' in - * the example above) - * - string-object: where the key contains other nested key value pairs (e.g. - * 'key-b' in the example above) - */ -class Entry { - /** - * Create a new `Entry` instance with the given key, value and comment. - * - * @param key {string} the key of the mapping - * @param comment {string | undefined} the documentation comment, if any - */ - constructor(key, comment = undefined) { - this.key = key // set to '' for the JSON top-level object - this.doc = comment - - this.value = undefined // populated for string-string entries, `undefined` otherwise - this.children = [] // populated for string-object entries, `[]` otherwise - this.parent = undefined - } - - /** - * Get the path to this entry as a list of path component strings. - * - * @return {string[]} the list of all path components up to this entry - */ - get path() { - const path = this.parent?.path ?? [] - path.push(this.key) - return path - } - - /** - * Get the fully qualified name of the instance w.r.t. the root `Entry`. The - * `path` field is sliced to remove the root key '' from the list. - * - * @return {string} the dot separated path to this entry - */ - get lineage() { - return this.path.slice(1).join(".") - } - - /** - * Register the given entry as a child of this one. Adds this entry as the - * child's parent. - * - * @param child {Entry} the child to register - */ - addChild(child) { - this.children.push(child) - child.parent = this - } - - /** - * Get the JSON representation of this entry and all its children. This - * conversion loses information present in comments. - * - * @return {SimJson} a POJSO containing the translation mappings - */ - toJSON() { - // This is a string-string entry, will be handled by parent. - if (this.value) { - return {} - } - - /** @type {SimJson} */ - const pojo = {} - for (const child of this.children) { - if (child.value) { - pojo[child.key] = child.value - } else { - pojo[child.key] = child.toJSON() - } - } - return pojo - } -} - -/** - * Determine the key for the entry, which will be a string literal if quoted, - * or an identifier if not quoted. - * - * @param keyNode {import('@babel/types').StringLiteral|import('@babel/types').Identifier} - * @return {string} the text content of the key - */ -const parseKey = (keyNode) => { - if (keyNode === undefined) { - return "" - } - switch (keyNode.type) { - case "StringLiteral": { - return keyNode.value - } - case "Identifier": { - return keyNode.name - } - } -} - -/** - * Parse a single or multi-line comment. If the comment is multi-line, newlines - * and asterisks will be removed from the output. - * - * @param commentNode {import('@babel/types').CommentLine|import('@babel/types').CommentBlock} - * @return {string} the text content of the comment - */ -const parseComment = (commentNode) => { - switch (commentNode.type) { - case "CommentLine": { - return commentNode.value.trim() - } - case "CommentBlock": { - return commentNode.value - .replace(/\n|\*+/g, "") - .replace(/\s+/g, " ") - .trim() - } - } -} - -/** - * Populate the value or children of the `Entry` depending on whether the value - * is a string or an object expression. - * - * @param entry {Entry} the entry to populate - * @param valueNode {import('@babel/types').StringLiteral|import('@babel/types').ObjectExpression} - */ -const parseValue = (entry, valueNode) => { - switch (valueNode.type) { - case "StringLiteral": { - entry.value = valueNode.value - break - } - case "ObjectExpression": { - valueNode.properties.map(parseObjProperty).forEach((child) => { - entry.addChild(child) - }) - break - } - } -} - -/** - * Create an `Entry` instance by parsing the AST node from Babel. - * - * @param node {import('@babel/types').ObjectProperty} - * @return {Entry} the entry generated by parsing the node - */ -const parseObjProperty = (node) => { - const key = parseKey(node.key) - const comments = node.leadingComments?.map(parseComment).join("") - const entry = new Entry(key, comments) - parseValue(entry, node.value) - return entry -} - -/** - * Parse the given filename into a tree of `Entry` instances. - * @param filename {string} the name of the JSON file to read - * @return {Entry} the root `Entry` instance for the top-level JSON object - */ -const parseJson = (filename) => - parseObjProperty({ - value: babel.parseExpression( - fs.readFileSync( - path.join(__dirname, "..", "..", "data", filename), - "utf-8" - ) - ), - }) - -module.exports = { Entry, parseJson } diff --git a/frontend/i18n/locales/scripts/types.d.ts b/frontend/i18n/locales/scripts/types.d.ts deleted file mode 100644 index 983c73e74f6..00000000000 --- a/frontend/i18n/locales/scripts/types.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface I18nLocaleProps { - code: string - name: string - wpLocale?: string - file?: string - language?: string - dir?: string - translated?: number -} diff --git a/frontend/i18n/locales/scripts/utils.js b/frontend/i18n/locales/scripts/utils.js deleted file mode 100644 index 4e18db89396..00000000000 --- a/frontend/i18n/locales/scripts/utils.js +++ /dev/null @@ -1,145 +0,0 @@ -const { writeFile } = require("fs/promises") -const os = require("os") - -/** - * Convert a kebab-case string (`image-title`) to camel case (`imageTitle`). - */ -function kebabToCamel(input) { - const split = input.split("-") - if (split.length === 1) { - return input - } - - for (let i = 1; i < split.length; i++) { - split[i] = split[i][0].toUpperCase() + split[i].slice(1) - } - return split.join("") -} - -/** - * Mutates an object at the path with the value. If the path - * does not exist, it is created by nesting objects along the - * path segments. - * - * @see {@link https://stackoverflow.com/a/20240290|Stack Overflow} - * - * @param {any} obj - The object to mutate. - * @param {string} path - The dot delimited path on the object to mutate. - * @param {unknown} value - The value to set at the path. - */ -exports.setToValue = function setValue(obj, path, value) { - const a = path.split(".") - let o = obj - while (a.length - 1) { - const n = a.shift() - if (!(n in o)) { - o[n] = {} - } - o = o[n] - } - o[a[0]] = value -} - -// function replacer(_, match) { -// // Replace ###### from `po` files with {} in `vue`. -// // Additionally, the old kebab-cased keys that can still be in the -// // translations are replaced with camelCased keys the app expects. -// if (match.includes("_")) { -// match.replace(/_/g, "-") -// console.warn("Found _ in translation strings:", match) -// } -// // TODO: Remove `camel` and warning once all translation strings are updated. -// // https://github.com/WordPress/openverse/issues/2438 -// if (match.includes("-")) { -// console.warn("Found kebab-cased key in translation strings:", match) -// } -// return `{${kebabToCamel(match)}}` -// } - -/** - * Replace ###### with {}. - * - * @param {any} json - the JSON object to replace placeholders in - * @param {string} locale - the locale of the JSON object - * @param {object} deprecatedKeys - object to store deprecated kebab-cased keys and number of replacements. - * @return {any} the sanitised JSON object - */ -const replacePlaceholders = (json, locale, deprecatedKeys) => { - if (json === null) { - return null - } - - /** - * Replaces ###### from `po` files with {} in `vue`. - * Additionally, the old kebab-cased keys that can still be in the - * translations are replaced with camelCased keys the app expects. - */ - function replacer(_, match) { - if (match.includes("-")) { - deprecatedKeys.count++ - deprecatedKeys.keys[locale] = [ - ...(deprecatedKeys.keys[locale] ?? []), - match, - ] - } - return `{${kebabToCamel(match)}}` - } - - if (typeof json === "string") { - if (json.includes("{") && json.includes("}")) { - json = json.replaceAll("{", "###") - json = json.replaceAll("}", "###") - } - if (json.includes("")) { - json = json.replaceAll("", "") - json = json.replaceAll("", "") - } - if (json.includes("||")) { - json = "" - } - - let replaced = json.replace(/###([a-zA-Z-]*?)###/g, replacer) - // Irregular placeholders with more or fewer than 3 #s - replaced = replaced.replace(/#{1,4}([a-zA-Z-]+?)#{1,4}/g, "{$1}") - if (replaced.includes("{}")) { - console.warn(`Found {} in ${locale} translation strings: ${replaced}`) - replaced = "" - } - const withoutOpenverseChannel = replaced.replace("#openverse", "") - if (withoutOpenverseChannel.includes("#")) { - console.warn( - `Found left-over # in ${locale} translation strings: ${replaced}` - ) - replaced = "" - } - return replaced - } - const currentJson = { ...json } - - for (const row of Object.entries(currentJson)) { - const [key, value] = row - currentJson[key] = replacePlaceholders(value, locale, deprecatedKeys) - } - return currentJson -} - -exports.replacePlaceholders = replacePlaceholders - -/** - * Write translation strings to a file in the locale directory - * @param {string} locale - * @param {any} rawTranslations - * @param {object} deprecatedKeys - object to store deprecated kebab-cased keys and number of replacements. - */ -exports.writeLocaleFile = (locale, rawTranslations, deprecatedKeys) => { - const translations = replacePlaceholders( - rawTranslations, - locale, - deprecatedKeys - ) - - return writeFile( - process.cwd() + `/i18n/locales/${locale}.json`, - JSON.stringify(translations, null, 2) + os.EOL - ) -} diff --git a/frontend/i18n/scripts/generate-pot.mjs b/frontend/i18n/scripts/generate-pot.mjs new file mode 100644 index 00000000000..3ade0d6c341 --- /dev/null +++ b/frontend/i18n/scripts/generate-pot.mjs @@ -0,0 +1,48 @@ +import { readFileSync, writeFileSync } from "fs" +import { join } from "path" +import { pathToFileURL } from "url" + +import { parseExpression } from "@babel/parser" + +import { baseDir, enJson5 } from "./paths.mjs" +import { jsonEntryToPot, nodeToEntry } from "./po/po-helpers.mjs" + +const pot_creation_date = () => + `${new Date().toISOString().split(".")[0]}+00:00` + +const POT_FILE_META = `# Copyright (C) 2021 +# This file is distributed under the same license as Openverse. +msgid "" +msgstr "" +"Project-Id-Version: Openverse \\n" +"Report-Msgid-Bugs-To: https://github.com/wordpress/openverse/issues \\n" +"POT-Creation-Date: ${pot_creation_date()}\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"PO-Revision-Date: 2021-MO-DA HO:MI+ZONE\\n" +"Last-Translator: FULL NAME \\n" +"Language-Team: LANGUAGE \\n" +` + +const convertJsonToPot = () => { + try { + const jsonContent = readFileSync(enJson5, "utf-8") + const expression = /** @type {import("@babel/types").ObjectExpression} */ ( + parseExpression(jsonContent) + ) + + const potContent = expression.properties + .map((child) => nodeToEntry(child)) + .map((entry) => jsonEntryToPot(entry)) + .join("\n\n") + + const potFile = join(baseDir, "openverse.pot") + writeFileSync(potFile, POT_FILE_META + potContent) + console.log(`Successfully wrote pot file to ${pathToFileURL(potFile)}`) + } catch (err) { + console.error(err) + } +} + +convertJsonToPot() diff --git a/frontend/i18n/scripts/metadata.mjs b/frontend/i18n/scripts/metadata.mjs new file mode 100644 index 00000000000..bb1636f2aa7 --- /dev/null +++ b/frontend/i18n/scripts/metadata.mjs @@ -0,0 +1,259 @@ +/** + This script extracts data for locales available in GlotPress and translate.wp.org, + transforms some locale properties to match what Vue i18n expects, + and saves it to `wp-locales-list.json`. + Updates the translation status of locales list from wp-locales-list, + and saves lists of translated and untranslated locales with properties expected + by Vue i18n. + */ +import { join } from "path" +import { copyFileSync, existsSync, readdirSync } from "fs" + +import { userAgentHeader } from "../../shared/constants/user-agent.mjs" + +import { readToJson, snakeToCamel } from "./utils.mjs" +import { i18nDataDir, localesDir, testLocalesDir } from "./paths.mjs" + +const base_url = + "https://raw.githubusercontent.com/GlotPress/GlotPress/refs/heads/develop/locales/locales.php" + +// Fix invalid locale slugs, see https://github.com/WordPress/openverse/issues/5059 +const LOCALES_FIX = { + kir: "ky", +} + +// Pattern to match all locales except English (US). We don't want to overwrite `en.json`, +// which is copied from `en.json5`. +const WP_LOCALE_PATTERN = /wp_locale *= *'(?!en_US\b)([^']+)';/ + +const DEFAULT_LOCALE_PROPERTIES = [ + "english_name", + "native_name", + "slug", // unique identifier used by Nuxt i18n, used for HTML lang attribute + "text_direction", + "nplurals", + "plural_expression", +] + +const createPropertyRePatterns = () => { + const propertyRePatterns = {} + const properties = process.argv.includes("--plural") + ? [...DEFAULT_LOCALE_PROPERTIES] + : [...DEFAULT_LOCALE_PROPERTIES].filter( + (prop) => !["plural_expression", "nplurals"].includes(prop) + ) + properties.forEach((prop) => { + propertyRePatterns[prop] = + prop === "nplurals" + ? new RegExp(`${prop} *= *(\\d+);`) + : new RegExp(`${prop} *= *'(.*)';`) + }) + return propertyRePatterns +} + +const PROPERTY_PATTERNS = createPropertyRePatterns() + +/** + * Fetches the data from GlotPress GitHub. + * @returns {Promise} + */ +async function fetchLocalesPhpFile() { + try { + const res = await fetch(base_url, { headers: userAgentHeader }) + return await res.text() + } catch (error) { + console.error("Failed to fetch locales.php from GlotPress", error) + return "" + } +} + +/** + * Parses the raw string for each locale and extracts the properties we need. + * Only saves the locales that are supported by https://translate.wordpress.org/ + * (that is, the locales that have a `wp_locale` property). + * + * @param rawData {string} + * @return {import("./types").RawI18nLocaleProps} + */ +function parseLocaleMetadata(rawData) { + const wpLocaleMatch = rawData.match(WP_LOCALE_PATTERN) + + if (wpLocaleMatch) { + const data = {} + + // Extract the values, convert the property names to camelCase, + // replace `english_name` with `name`, and remove the quotes around pluralization values. + Object.keys(PROPERTY_PATTERNS).forEach((key) => { + const value = rawData.match(PROPERTY_PATTERNS[key]) + if (value) { + const propName = key === "english_name" ? "name" : snakeToCamel(key) + if (propName === "nplurals") { + data[propName] = Number.parseInt(value[1], 10) + } else { + data[propName] = value[1] + } + } + }) + return /** @type import("./types").RawI18nLocaleProps */ (data) + } +} + +/** + * Fetches locale data from the GlotPress GitHub repository. + * Extracts properties, converts locale property names to camelCase as expected + * by Vue i18n, and adds `code` property to each locale. + * @returns {Promise} + */ +export async function fetchAndParseLocaleMetadata() { + const data = await fetchLocalesPhpFile() + + // Splits the text by the new locale declaration string, and extracts locale metadata + // from each locale record. + const supportedLocales = data + .split("new GP_Locale();") + .splice(1) + .map((item) => item.trim()) + .map(parseLocaleMetadata) + .filter(Boolean) + + console.log( + `${supportedLocales.length} locales available on https://translate.wordpress.org found in GlotPress repository.` + ) + + return supportedLocales +} + +/** + * Computes the `translated` percentage for a locale. If the locale json file does not + * exist, returns 0. + * Otherwise, counts the number of keys in the locale file, and computes the `translated` + * percentage using the total number of keys in the original `en.json5` file. + * @param jsonFilePath - the current locale file name. + * @param totalKeysCount - the total number of keys in the `en.json5` file. + * @return {number} + */ +const getTranslatedPercentage = (jsonFilePath, totalKeysCount) => { + const fileLocation = join(localesDir, jsonFilePath) + if (!existsSync(fileLocation)) { + return 0 + } + const jsonKeysCount = Object.keys(readToJson(fileLocation)).length + return Math.ceil((jsonKeysCount * 100) / totalKeysCount) +} + +/** + * Returns a list of locale objects with at least one translated string + * @returns {Promise} + */ +export const getWpLocalesMetadata = async ( + totalKeysCount, + debug, + getPluralization = false +) => { + const localesList = await fetchAndParseLocaleMetadata() + if (getPluralization) { + getPluralizationRules(localesList) + } + + const result = { + translated: [], + untranslated: [], + } + Object.values(localesList).map((locale) => { + if (locale.slug in LOCALES_FIX) { + const fixedSlug = LOCALES_FIX[locale.slug] + if (debug) { + console.log( + `Changing invalid locale slug ${locale.slug} to ${fixedSlug}` + ) + } + locale.slug = fixedSlug + } + const localeFile = `${locale.slug}.json` + + const translated = getTranslatedPercentage(localeFile, totalKeysCount) + + const localeProperties = { + /* Nuxt i18n fields */ + code: locale.slug, + dir: locale.textDirection || "ltr", + file: localeFile, + // Used for the html lang attribute. + language: locale.slug, + + /* Custom fields */ + name: locale.name, + nativeName: locale.nativeName || locale.name, + translated, + } + if (translated > 0) { + result.translated.push(localeProperties) + } else { + result.untranslated.push(localeProperties) + } + }) + + return result +} + +/** + * Some locales have custom pluralization rules that differ from the default + * rules provided by Vue i18n. This function extracts the pluralization rules + * from the locales metadata and logs them in a format that can be copied to + * the `vue-i18n.ts` settings file. + * There shouldn't be a need to update it often, but it's here for reference. + * @param {import('./types').RawI18nLocaleProps[]} locales + */ +const getPluralizationRules = (locales) => { + const rules = {} + for (const locale of locales) { + // For Vue, the default pluralization rules handle cases where + // - nplurals=2 and pluralExpression="n > 1" + // - nplurals=1 and pluralExpression="0" + // We don't need to include these rules in the settings file. + const isDefaultPluralization = + (!locale.nplurals && !locale.pluralExpression) || + (locale.nplurals === 2 && locale.pluralExpression === "n > 1") || + (locale.nplurals === 1 && locale.pluralExpression === "0") + if (!isDefaultPluralization) { + if (locale.nplurals === 2) { + // When nplurals is 2, the plural expression is a boolean expression. + // Convert it to return `0` or `1`. + const isBoolean = + typeof eval(locale.pluralExpression.replaceAll("n", "1")) === + "boolean" + if (isBoolean) { + locale.pluralExpression = `(${locale.pluralExpression}) ? 1 : 0` + } + } + rules[locale.slug] = `(n: number): number => ${locale.pluralExpression},` + } + } + const pluralRulesFiles = Object.entries(rules) + .map(([key, value]) => `${key}: ${value}`) + .join("\n") + + console.log(pluralRulesFiles) +} + +/** + * Copies the test translation files and the corresponding `valid-locales.json` + * file to the `i18n/locales` directory for running Playwright tests. + * @return {Promise} + */ +export async function copyTestLocalesMetadata() { + console.log("Copying translations from the test folder...") + const files = readdirSync(testLocalesDir) + + await Promise.all( + files.map(async (file) => { + const sourcePath = join(testLocalesDir, file) + const destPath = + file === "valid-locales.json" + ? join(i18nDataDir, file) + : join(localesDir, file) + copyFileSync(sourcePath, destPath) + }) + ) + console.log("Done copying!") +} diff --git a/frontend/i18n/scripts/paths.mjs b/frontend/i18n/scripts/paths.mjs new file mode 100644 index 00000000000..ebd1efe3754 --- /dev/null +++ b/frontend/i18n/scripts/paths.mjs @@ -0,0 +1,19 @@ +import { dirname, join } from "path" +import { fileURLToPath } from "url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) +export const baseDir = join(__dirname, "..", "..") +export const srcDir = join(baseDir, "src") +export const testLocalesDir = join(baseDir, "test", "locales") +export const i18nDir = join(baseDir, "i18n") + +export const localesDir = join(i18nDir, "locales") +export const i18nDataDir = join(i18nDir, "data") + +export const enJson = /** @type {`${string}.json`} */ ( + join(localesDir, "en.json") +) +export const enJson5 = /** @type {`${string}.json5`} */ ( + join(i18nDataDir, "en.json5") +) +export const validLocales = join(i18nDataDir, "valid-locales.json") diff --git a/frontend/i18n/locales/scripts/parse-vue-files.js b/frontend/i18n/scripts/po/parse-vue-files.mjs similarity index 58% rename from frontend/i18n/locales/scripts/parse-vue-files.js rename to frontend/i18n/scripts/po/parse-vue-files.mjs index 7b313fe7336..928e9a2ad21 100644 --- a/frontend/i18n/locales/scripts/parse-vue-files.js +++ b/frontend/i18n/scripts/po/parse-vue-files.mjs @@ -1,32 +1,40 @@ // This implementation is loosely copied from vue-i18n-extract // https://github.com/pixari/vue-i18n-extract +import { readFileSync } from "fs" +import { join } from "path" -const fs = require("fs") -const path = require("path") +import { glob } from "glob" -const glob = require("glob") +import { srcDir } from "../paths.mjs" -const BASE_PATH = path.join( - path.dirname(path.dirname(path.dirname(__dirname))), - "src" -) - -function readVueFiles(src) { - const targetFiles = glob.sync(src) +/** + * Parses all vue files found in the glob paths, and returns an + * array of objects with i18n path, line number, and vue file path. + * { + path: 'browsePage.aria.close', + line: 13, + file: '/components/AppModal.vue' + }, + * from the BASE_PATH (`openverse/frontend/src`) + * @return {Array} + */ +export const getParsedVueFiles = () => { + // Look for .vue and .js files in the src directory + const pattern = join(srcDir, "**/*.?(js|vue)") + const targetFiles = glob.sync(pattern) if (targetFiles.length === 0) { throw new Error("vueFiles glob has no files.") } - // Now that the script are inside `i18n/locales/scripts`, - // to get relative URL, the script needs to go up 4 levels - return targetFiles.map((f) => { - const fileName = path.relative(process.cwd(), f.replace(BASE_PATH, "src")) + const filesList = targetFiles.map((f) => { + const fileName = f.replace(srcDir, "src") return { fileName, path: f, - content: fs.readFileSync(f, "utf8"), + content: readFileSync(f, "utf8"), } }) + return extractI18nItemsFromVueFiles(filesList) } function* getMatches(file, regExp, captureGroup = 1) { @@ -50,9 +58,9 @@ function* getMatches(file, regExp, captureGroup = 1) { /** * Extracts translation keys from methods such as `$t` and `$tc`. * - * - **regexp pattern**: (?:[$ .]tc?)\( + * - **regexp pattern**: (?:[$ .]t)\( * - * **description**: Matches the sequence t( or tc(, optionally with either “$”, “.” or “ ” in front of it. + * **description**: Matches the sequence t( optionally with either “$”, “.” or “ ” in front of it. * * - **regexp pattern**: (["'`]) * @@ -72,54 +80,19 @@ function* getMatches(file, regExp, captureGroup = 1) { */ function extractMethodMatches(file) { - const methodRegExp = /(?:[$ .]tc?)\(\s*?(["'`])((?:[^\\]|\\.)*?)\1/g + const methodRegExp = /[$ .]t\(\s*?(["'`])((?:[^\\]|\\.)*?)\1/g return [...getMatches(file, methodRegExp, 2)] } function extractComponentMatches(file) { - const componentRegExp = /(?: { const methodMatches = extractMethodMatches(file) const componentMatches = extractComponentMatches(file) - const directiveMatches = extractDirectiveMatches(file) - return [ - ...accumulator, - ...methodMatches, - ...componentMatches, - ...directiveMatches, - ] + return [...accumulator, ...methodMatches, ...componentMatches] }, []) } - -function parseVueFiles(vueFilesPath) { - const filesList = readVueFiles(vueFilesPath) - return extractI18nItemsFromVueFiles(filesList) -} - -/** - * Parses all vue files found in the glob paths, and returns an - * array of objects with i18n path, line number, and vue file path. - * { - path: 'browsePage.aria.close', - line: 13, - file: '/components/AppModal.vue' - }, - * @param {string} vueFiles - glob pattern to find all the vue files, - * from the BASE_PATH (`openverse/frontend/src`) - * @return {Array} - */ -const getParsedVueFiles = (vueFiles) => { - const resolvedVueFiles = path.resolve(BASE_PATH, vueFiles) - return parseVueFiles(resolvedVueFiles) -} - -module.exports = { getParsedVueFiles } diff --git a/frontend/i18n/scripts/po/po-helpers.mjs b/frontend/i18n/scripts/po/po-helpers.mjs new file mode 100644 index 00000000000..2f7fbb7bb47 --- /dev/null +++ b/frontend/i18n/scripts/po/po-helpers.mjs @@ -0,0 +1,177 @@ +/** + * More about the structure of .po files: + * // https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files + * + * ```po + * white-space + * # translator-comments + * #. extracted-comments + * #: reference… + * #, flag… + * #| msgid previous-untranslated-string + * msgid untranslated-string + * msgstr translated-string + * ``` + */ +import { getParsedVueFiles } from "./parse-vue-files.mjs" + +const PARSED_VUE_FILES = getParsedVueFiles() + +/** @param str {string} */ +const escapeQuotes = (str) => str.replace(/"/g, '\\"') + +/** @param str {string} */ +const containsCurlyWord = (str) => /\{[a-zA-Z-]*}/.test(str) + +/** @param str {string} */ +const checkStringForVars = (str) => + containsCurlyWord(str) ? "#. Do not translate words between ### ###." : "" + +/** + * For GlotPress to display warning when the translators miss the placeholders + * or try replacing them with something else, we need to surround the + * placeholders with `###`. + * + * @param str {string} the translation string + * @return {string} the translation string with all placeholders marked + */ +const replaceVarsPlaceholders = (str) => { + if (!containsCurlyWord(str)) { + return str + } + + const variable = /\{(?[a-zA-Z-]*)}/g + return str.replace(variable, `###$###`) +} + +/** + * Replace placeholder format for variables and escape quotes. + * + * @param str {string} the translation string + * @return {string} the translation string with quotes escaped and placeholders marked + */ +const processValue = (str) => escapeQuotes(replaceVarsPlaceholders(str)) + +/** + * Returns a comment with all reference to the file and line where the string is + * used. These are prefixed with `#:`. + * + * @param keyPath {string} the lineage of the entry to search in Vue files + * @return {string[]} the list of reference comments + */ +const getRefComments = (keyPath) => + PARSED_VUE_FILES.filter((k) => k.path === keyPath).map( + (item) => `#: ${item.file}:${item.line}` + ) + +/** + * Generate the comment for the POT entry. This includes any comment written on + * the JSON entry, a message about `###` and finally references to where that + * entry is used in the codebase. + * + * @return {string} the comment lines + */ +export const createComment = (key, comment, val) => { + const res = [] + + // comments given by the programmer, directed at the translator (#.) + if (comment) { + res.push(`#. ${comment}`) + } + + // comments given by the programmer, directed at the translator (#.) + const vars = checkStringForVars(val) + if (vars) { + res.push(vars) + } + + // comments containing references to the program’s source code (#:) + const refComments = getRefComments(key) + if (refComments.length) { + res.push(...refComments) + } + + return res.map((item) => `${item}`).join("\n") +} + +const generatePluralizationLines = (value) => { + const pluralizedValues = value.split("|") + if (pluralizedValues.length === 1) { + return pluralizedValues + } + return [ + `msgid "${processValue(pluralizedValues[0])}"`, + `msgid_plural "${processValue(pluralizedValues[1])}"`, + 'msgstr[0] ""', + 'msgstr[1] ""', + ] +} +/** + * Convert a JSON entry into a string for the POT file. This includes the + * message context, the message id and the message string (with pluralization + * if needed). + * + * @param entry {import("./types").JsonEntry} the JSON entry to convert + * @return {string} the POT equivalent of the JSON entry + */ +export const jsonEntryToPot = ({ key, value, doc }) => { + const poEntry = [] + if (doc) { + poEntry.push(doc) + } + poEntry.push(`msgctxt "${key}"`) + if (value.includes("|") && /(count|time)/i.test(value)) { + poEntry.push(...generatePluralizationLines(value)) + } else { + poEntry.push(`msgid "${processValue(value)}"`, 'msgstr ""') + } + return poEntry.join("\n") +} + +/** + * Parse a single or multi-line comment. If the comment is multi-line, newlines + * and asterisks will be removed from the output. + * + * @param commentNode {import('@babel/types').CommentLine|import('@babel/types').CommentBlock} + * @return {string} the text content of the comment + */ +export const parseComment = (commentNode) => { + switch (commentNode.type) { + case "CommentLine": { + return commentNode.value.trim() + } + case "CommentBlock": { + return commentNode.value + .replace(/\n|\*+/g, "") + .replace(/\s+/g, " ") + .replace('"', '\\"') + .trim() + } + } +} + +/** + * Convert a JSON entry into a object with key, value and the linked doc comment. + * @param node {import('@babel/types').ObjectProperty | import('@babel/types').ObjectMethod | import('@babel/types').SpreadElement } + * @return {import("./types").JsonEntry} + */ +export const nodeToEntry = (node) => { + let key + switch (node.key.type) { + case "StringLiteral": { + key = node.key.value + break + } + case "Identifier": { + key = node.key.name + break + } + } + if (!key || typeof key !== "string") { + throw new Error(`Invalid key ${key} in node ${node.toString()}`) + } + const value = node.value.value ?? "" + const parsedComment = node.leadingComments?.map(parseComment).join("") + + return { key, value, doc: createComment(key, parsedComment, value) } +} diff --git a/frontend/i18n/scripts/setup.mjs b/frontend/i18n/scripts/setup.mjs new file mode 100644 index 00000000000..dda5b053393 --- /dev/null +++ b/frontend/i18n/scripts/setup.mjs @@ -0,0 +1,115 @@ +import { join } from "path" +import { existsSync, mkdirSync, writeFileSync } from "fs" +import { pathToFileURL } from "url" + +import { watch } from "chokidar" + +import { prettify, writeJson } from "./utils.mjs" +import { downloadTranslations, writeEnglish } from "./translations.mjs" +import { + enJson5 as enJson5File, + i18nDataDir, + localesDir, + validLocales, +} from "./paths.mjs" +import { copyTestLocalesMetadata, getWpLocalesMetadata } from "./metadata.mjs" + +/** + * Sets up the data for i18n in Openverse: + * - Copies en.json5 to en.json + * - Downloads translations from GlotPress if necessary + * - Generates locales metadata for Vue i18n for the locales with translations + * - For tests, copies the existing metadata from the test `locales` directory. + * - For development, watches the `en.json5` file for changes and updates `en.json` accordingly. + * @param options + * @return {Promise} + */ +const setupI18n = async (options) => { + console.log("Setting up i18n") + + if (!existsSync(localesDir)) { + console.log("Creating locales directory...") + mkdirSync(localesDir) + } + + // Count the number of English strings to calculate translated percentage later + let totalKeysCount = 0 + + // Copy en.json5 to en.json + try { + totalKeysCount = writeEnglish(true) + console.log("Copied en.json5 to en.json") + } catch (error) { + console.error("Failed to copy en.json5 to en.json:", error) + process.exit(1) + } + + if (options.enOnly) { + writeJson(validLocales, []) + return + } + + if (options.test) { + await copyTestLocalesMetadata() + return + } + if (options.noGet) { + console.log("Skipping download, re-using existing translation files.") + } else { + console.log("Getting translations...") + const success = await downloadTranslations(!!options.verbose) + if (!success) { + process.exit(1) + } + } + console.log("Starting locale metadata generation...") + const localesMetadata = await getWpLocalesMetadata( + totalKeysCount, + options.verbose, + options.getPluralizations + ) + const files = { + translated: { + path: validLocales, + data: localesMetadata.translated, + count: localesMetadata.translated.length, + }, + untranslated: { + path: join(i18nDataDir, "untranslated-locales.json"), + data: localesMetadata.untranslated, + count: localesMetadata.untranslated.length, + }, + } + for (const [type, { path, data, count }] of Object.entries(files)) { + writeFileSync(path, prettify(data)) + console.log( + `Wrote metadata for ${count} ${type} locales to ${pathToFileURL(path)}.` + ) + } +} + +const options = { + enOnly: process.argv.includes("--en-only"), + watch: process.argv.includes("--watch"), + noGet: process.argv.includes("--no-get"), + requireComplete: process.argv.includes("--require-complete"), + verbose: process.argv.includes("--debug"), + test: process.argv.includes("--test"), + getPlurals: process.argv.includes("--plural"), +} + +setupI18n(options) + .then(() => { + console.log("i18n setup complete.") + if (options.watch) { + console.log("Watching en.json5 for changes...") + watch(enJson5File).on("all", (event, path) => { + console.log(`Event '${event}' for file ${path}`) + writeEnglish() + }) + } + }) + .catch((error) => { + console.error("i18n setup failed:", error) + if (options.requireComplete) process.exitCode = 1 + }) diff --git a/frontend/i18n/scripts/translations.mjs b/frontend/i18n/scripts/translations.mjs new file mode 100644 index 00000000000..2aa5d697acf --- /dev/null +++ b/frontend/i18n/scripts/translations.mjs @@ -0,0 +1,231 @@ +import { createWriteStream, readFileSync, unlinkSync } from "fs" +import { basename, join } from "path" +import { pipeline } from "stream/promises" + +import AdmZip from "adm-zip" +import json5 from "json5" + +import { userAgentHeader } from "../../shared/constants/user-agent.mjs" + +import { kebabToCamel, prettify, readToJson, writeJson } from "./utils.mjs" +import { + enJson as enJsonFile, + enJson5 as enJson5File, + localesDir, +} from "./paths.mjs" + +const NGX_URL = + "https://translate.wordpress.org/exporter/meta/openverse/-do/?export-format=ngx" + +const fetchBulkNgx = async () => { + const zipPath = join(localesDir, "openverse.zip") + const res = await fetch(NGX_URL, { headers: userAgentHeader }) + + if (!res.ok) { + throw new Error( + `Failed to download translations: ${res.status}, ${res.statusText}` + ) + } + + // Create write stream + const fileStream = createWriteStream(zipPath) + + // Use pipeline to handle the stream + await pipeline(res.body, fileStream) + + return zipPath +} + +/** + * Removes entries with null values from a flat json object. + * @param {Record} obj - The object to clean + * @returns {Record} - The cleaned object + */ +const removeNullValues = (obj) => { + const cleaned = {} + + for (const [key, value] of Object.entries(obj)) { + if (value !== null) { + cleaned[key] = value + } + } + + return cleaned +} + +const issueUrl = "https://github.com/WordPress/openverse/issues/" +const kebabCaseIssue = `${issueUrl}2438` +const invalidPlaceholderIssue = `${issueUrl}5149` + +const warnings = { + kebabCase: { + failure: `deprecated kebab-case keys replaced in locale files. +To see the keys, run \`ov just frontend/run i18n --debug\`. For more details, see ${kebabCaseIssue}.`, + success: `No deprecated kebab-case keys found in locale files. 🎉 Please close issue ${kebabCaseIssue}.`, + }, + invalidPlaceholders: { + failure: `invalid translation keys with extra # or empty {} symbols replaced in locale files. +To see the keys, run \`ov just frontend/run i18n --debug\`. For more details, see ${invalidPlaceholderIssue}.`, + success: `No invalid translation keys found in locale files. 🎉 Please close issue ${invalidPlaceholderIssue}.`, + }, +} + +const logWarnings = (deprecatedKeys, invalidKeys, debug) => { + for (const [keysKind, keysObject] of [ + ["kebabCase", deprecatedKeys], + ["invalidPlaceholders", invalidKeys], + ]) { + if (keysObject.count > 0) { + console.warn(`${keysObject.count} ${warnings[keysKind].failure}`) + if (debug) { + console.log(prettify(keysObject.keys)) + } + } else { + console.log(keysObject[keysKind].success) + } + } +} + +const extractZip = async (zipPath, debug) => { + const zip = new AdmZip(zipPath, {}) + let count = 0 + + const deprecatedKeys = { count: 0, keys: {} } + const invalidKeys = { count: 0, keys: {} } + + const getTargetName = (fileName) => { + const renamed = fileName.replace("meta-openverse-", "").replace(".ngx", "") + // Fix for invalid locale slug, see https://github.com/WordPress/openverse/issues/5059 + if (renamed.includes("kir.json")) { + return renamed.replace("kir.json", "ky.json") + } + return renamed + } + + zip.getEntries().forEach((entry) => { + if (entry.entryName.endsWith(".json")) { + const fileName = basename(entry.entryName) + const targetName = getTargetName(fileName) + const targetPath = join(localesDir, targetName) + + // Extract to a temporary location first + zip.extractEntryTo(entry, localesDir, false, true, true, targetName) + + const extractedPath = join(localesDir, targetName) + const content = readToJson(extractedPath) + + const nonNullContent = removeNullValues(content) + const cleanedContent = replacePlaceholders( + nonNullContent, + targetName, + deprecatedKeys, + invalidKeys + ) + + // Only save if there are translations + if (Object.keys(cleanedContent).length > 0) { + if (debug) { + console.log(`Writing ${targetPath}`) + } + writeJson(targetPath, cleanedContent) + count++ + } else { + // Remove the temporary file + unlinkSync(extractedPath) + } + } + }) + logWarnings(deprecatedKeys, invalidKeys, debug) + + return count +} + +/** + * Replace ###### with {}. + * + * @param {any} json - the JSON object to replace placeholders in + * @param {string} locale - the locale of the JSON object + * @param {object} deprecatedKeys - object to store deprecated kebab-cased keys and number of replacements. + * @param {object} invalidKeys - object to store invalid values that contain extra `#` or `{}`, and number of replacements. + * @return {any} the sanitised JSON object + */ +const replacePlaceholders = (json, locale, deprecatedKeys, invalidKeys) => { + const recordProblems = (key, problems) => { + problems.count++ + problems.keys[locale] = [...(problems.keys[locale] ?? []), key] + } + /** + * Replaces ###### from `po` files with {} in `vue`. + * Additionally, the old kebab-cased keys that can still be in the + * translations are replaced with camelCased keys the app expects. + */ + function replacer(_, match) { + if (match.includes("-")) { + recordProblems(match, deprecatedKeys) + } + return `{${kebabToCamel(match)}}` + } + + function cleanupString(str, key) { + if (str.includes("||")) { + return "" + } + const cleaned = str + .replace(/[{}]/g, "###") // Replace all { and } with ### + .replace(/<\/?em>/g, "") // Remove and tags + + let replaced = cleaned.replace(/###([a-zA-Z-]*?)###/g, replacer) + // Irregular placeholders with more or fewer than 3 #s + replaced = replaced.replace(/#{1,4}([a-zA-Z-]+?)#{1,4}/g, "{$1}") + + if (replaced.includes("{}")) { + recordProblems(`${key}:${cleaned}`, invalidKeys) + replaced = "" + } + const withoutOpenverseChannel = replaced.replace("#openverse", "") + if (withoutOpenverseChannel.includes("#")) { + recordProblems(`${key}:${cleaned}`, invalidKeys) + replaced = "" + } + return replaced + } + + for (const [key, value] of Object.entries(json)) { + json[key] = cleanupString(value, key) + } + return json +} + +/** + * Perform a bulk download of translation strings from GlotPress and extract the + * JSON files from the ZIP archive. + * + * @return {Promise} - whether the bulk download succeeded + */ +export const downloadTranslations = async (debug) => { + console.log("Performing bulk translations download.") + try { + const zipPath = await fetchBulkNgx() + const translationsCount = await extractZip(zipPath, debug) + unlinkSync(zipPath) + console.log(`Successfully saved ${translationsCount} translations.`) + return true + } catch (error) { + console.error("Failed to download translations:", error) + return false + } +} +/** + * Convert en.json5 to en.json, and return the number of keys in the file, + * if `countKeys` is true. + * @param {boolean} countKeys + * @return {number} + */ +export const writeEnglish = (countKeys = false) => { + const enJson5 = readFileSync(enJson5File, "utf8") + const fileContents = json5.parse(enJson5) + writeJson(enJsonFile, fileContents) + if (countKeys) { + return Object.keys(fileContents).length + } +} diff --git a/frontend/i18n/scripts/types.d.ts b/frontend/i18n/scripts/types.d.ts new file mode 100644 index 00000000000..dee1c6fd188 --- /dev/null +++ b/frontend/i18n/scripts/types.d.ts @@ -0,0 +1,48 @@ +export type TextDirection = "ltr" | "rtl" + +/** + * These properties are available in the GlotPress source code: + * https://raw.githubusercontent.com/GlotPress/GlotPress/refs/heads/develop/locales/locales.php + * They are used to generate the metadata that the Nuxt app uses, `I18nLocaleProps`. + */ +export interface RawI18nLocaleProps { + name: string + nativeName: string + /** The WordPress locale code that is used as the locale's URL prefix */ + slug: string + textDirection: TextDirection + nplurals?: number + pluralExpression?: string +} + +export interface RawI18nLocalePropsWithPlurals + extends Omit { + nplurals: number + /** + * The plural expression used for custom pluralization, ee the `vue-i18n.ts` settings file + */ + pluralExpression: string +} + +type LocaleCode = string + +export interface I18nLocaleProps { + name: string + nativeName: string + code: LocaleCode + dir: TextDirection + file: `${LocaleCode}.json` + language: string + translated: number +} + +export interface JsonEntry { + key: string + value: string + doc: string +} + +export interface LocalesByTranslation { + translated: I18nLocaleProps[] + untranslated: I18nLocaleProps[] +} diff --git a/frontend/i18n/scripts/utils.mjs b/frontend/i18n/scripts/utils.mjs new file mode 100644 index 00000000000..11464eac421 --- /dev/null +++ b/frontend/i18n/scripts/utils.mjs @@ -0,0 +1,40 @@ +import { readFileSync, writeFileSync } from "fs" +import { EOL } from "os" + +export const snakeToCamel = (str) => + str + .toLowerCase() + .replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace("-", "").replace("_", "") + ) + +/** + * Convert a kebab-case string (`image-title`) to camel case (`imageTitle`). + */ +export function kebabToCamel(input) { + const split = input.split("-") + if (split.length === 1) { + return input + } + + for (let i = 1; i < split.length; i++) { + split[i] = split[i][0].toUpperCase() + split[i].slice(1) + } + return split.join("") +} + +export const prettify = (json) => JSON.stringify(json, null, 2) + +/** + * Write JSON data to a file with consistent formatting. + */ +export const writeJson = (fileName, data) => { + writeFileSync(fileName, prettify(data) + EOL, { + encoding: "utf-8", + flag: "w+", + }) +} + +export const readToJson = (filePath) => { + return JSON.parse(readFileSync(filePath, "utf-8")) +} diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 7e4f20d2b09..c7fe95889ce 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,7 +1,7 @@ import { defineNuxtConfig } from "nuxt/config" import { disallowedBots } from "./shared/constants/disallowed-bots" -import locales from "./i18n/locales/scripts/valid-locales.json" +import locales from "./i18n/data/valid-locales.json" import type { LocaleObject } from "@nuxtjs/i18n" diff --git a/frontend/package.json b/frontend/package.json index 63af396d0a6..5c6b70794c9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,12 +16,12 @@ "generate": "nuxt generate", "start": "PORT=\"${PORT:-8443}\" CONSOLA_LEVEL=\"${CONSOLA_LEVEL:-1}\" node .output/server/index.mjs", "start:mem": "PORT=\"${PORT:-8443}\" CONSOLA_LEVEL=\"${CONSOLA_LEVEL:-1}\" node --inspect .output/server/index.mjs", - "start:playwright": "pnpm i18n:copy-test-locales && pnpm start", + "start:playwright": "pnpm i18n:test && pnpm start", "prod": "pnpm build && pnpm start", - "prod:playwright": "pnpm i18n:copy-test-locales && pnpm prod", - "prod:storybook": "pnpm i18n:copy-test-locales && pnpm storybook:build && pnpm storybook:start", + "prod:playwright": "pnpm i18n:test && pnpm prod", + "prod:storybook": "pnpm i18n:test && pnpm storybook:build && pnpm storybook:start", "storybook": "storybook dev --port 54000", - "storybook:build": "pnpm i18n:copy-test-locales && storybook build", + "storybook:build": "pnpm i18n:test && storybook build", "storybook:start": "pnpx http-server storybook-static -p 54000 -s", "talkback": "node ./test/proxy.js", "prepare:nuxt": "pnpm i18n:en && npx nuxi prepare", @@ -41,15 +41,13 @@ "test:storybook:local": "playwright test -c test/storybook", "test:storybook:debug": "PWDEBUG=1 pnpm test:storybook:local", "test:storybook:gen": "playwright codegen localhost:54000/", - "types": "pnpm run i18n:copy-test-locales && pnpm run prepare:nuxt && vue-tsc -p .", - "i18n": "pnpm i18n:create-locales-list && pnpm i18n:get-translations && pnpm i18n:update-locales", - "i18n:en": "pnpm i18n:get-translations --en-only", - "i18n:copy-test-locales": "cp test/locales/**.json i18n/locales/ && mv i18n/locales/valid-locales.json i18n/locales/scripts/valid-locales.json", - "i18n:no-get": "pnpm i18n:create-locales-list && pnpm i18n:update-locales", - "i18n:create-locales-list": "node i18n/locales/scripts/create-wp-locale-list", - "i18n:get-translations": "node i18n/locales/scripts/get-translations", - "i18n:update-locales": "node i18n/locales/scripts/get-validated-locales", - "i18n:generate-pot": "node i18n/locales/scripts/json-to-pot", + "types": "pnpm run i18n:test && npx nuxi prepare && vue-tsc -p .", + "i18n": "node i18n/scripts/setup.mjs", + "i18n:debug": "pnpm i18n --debug", + "i18n:en": "pnpm i18n --en-only", + "i18n:test": "pnpm i18n --test", + "i18n:no-get": "pnpm i18n --no-get", + "i18n:generate-pot": "node i18n/scripts/generate-pot.mjs", "create:component-sfc": "remake component", "create:story": "remake story", "create:component-storybook-test": "remake component-storybook-test", @@ -106,6 +104,7 @@ "core-js": "^3.37.1", "eslint-plugin-jsonc": "^2.16.0", "jsdom": "^25.0.0", + "json5": "^2.2.3", "node-html-parser": "^6.1.13", "npm-run-all2": "^7.0.0", "nuxt": "^3.14.1592", diff --git a/frontend/shared/constants/user-agent.js b/frontend/shared/constants/user-agent.mjs similarity index 64% rename from frontend/shared/constants/user-agent.js rename to frontend/shared/constants/user-agent.mjs index 59bdea2b34b..0707cf05ade 100644 --- a/frontend/shared/constants/user-agent.js +++ b/frontend/shared/constants/user-agent.mjs @@ -1,9 +1,7 @@ /** * A user agent string by which Openverse.org identifies itself to services */ -const userAgent = +export const userAgent = "Openverse/0.1 (https://openverse.org; openverse@wordpress.org)" -module.exports = { - userAgent, -} +export const userAgentHeader = { "User-Agent": userAgent } diff --git a/frontend/shared/utils/attribution-html.ts b/frontend/shared/utils/attribution-html.ts index 2d47ad10db1..5a7feb6bb70 100644 --- a/frontend/shared/utils/attribution-html.ts +++ b/frontend/shared/utils/attribution-html.ts @@ -90,9 +90,24 @@ const fmt = (text: string, replacements: Record): string => { return text } +const isObject = (value: unknown): value is Record => { + return typeof value === "object" && value !== null +} + +const isBabelValue = (value: unknown): value is { loc: { source: string } } => { + return ( + isObject(value) && + "loc" in value && + isObject(value.loc) && + "source" in value.loc && + typeof value.loc.source === "string" + ) +} + /** * Perform the same role as `i18n.t` except that it's restricted to reading - * English from `en.json`. + * English from `en.json`. In unit tests, the json file is parsed by babel, + * so we handle the parsed objects, too. * * @param path - the key to the JSON value to read * @param replacements - the pair of placeholders and their replacement strings @@ -102,22 +117,15 @@ const fakeT = ( path: string, replacements: Record = {} ): string => { - interface NestedRecord { - [key: string]: string | NestedRecord + // enJson is parsed by babel in unit tests, so it returns a parsed object + const jsonValue = enJson[path as keyof typeof enJson] + if (isBabelValue(jsonValue)) { + return fmt(jsonValue.loc.source, replacements) + } else if (typeof jsonValue === "string") { + return fmt(jsonValue, replacements) + } else { + return "" } - - const segments = path.split(".") - let fraction: NestedRecord = enJson["mediaDetails"].reuse.credit - let text: string | undefined = undefined - segments.forEach((segment) => { - const piece = fraction[segment] - if (typeof piece === "string") { - text = piece - } else { - fraction = piece - } - }) - return text ? fmt(text, replacements) : "" } /** @@ -221,7 +229,8 @@ export const getAttribution = ( values ? t(`${i18nBase}.${key}`, values as Record) : t(`${i18nBase}.${key}`) - : fakeT + : (key: string, values?: Record) => + fakeT(`${i18nBase}.${key}`, values as Record) /* Title */ diff --git a/frontend/src/data/api-service.ts b/frontend/src/data/api-service.ts index f6451550c0c..a74cd4f827a 100644 --- a/frontend/src/data/api-service.ts +++ b/frontend/src/data/api-service.ts @@ -3,6 +3,7 @@ import { useRuntimeConfig } from "#imports" import axios, { AxiosRequestConfig, AxiosResponse } from "axios" import { AUDIO, type SupportedMediaType } from "#shared/constants/media" +import { userAgentHeader } from "#shared/constants/user-agent.mjs" import { mediaSlug } from "#shared/utils/query-utils" import type { Events } from "#shared/types/analytics" import type { Media } from "#shared/types/media" @@ -45,8 +46,6 @@ export interface MediaResult< results: T } -const userAgent = - "Openverse/0.1 (https://openverse.org; openverse@wordpress.org)" /** * Decodes the text data to avoid encoding problems. * Also, converts the results from an array of media @@ -145,13 +144,11 @@ export const createApiClient = ({ const baseUrl = useRuntimeConfig().public.apiUrl ?? "https://api.openverse.org/" - const headers: AxiosRequestConfig["headers"] = {} - if (import.meta.server) { - headers["User-Agent"] = userAgent - } - if (accessToken) { - headers["Authorization"] = `Bearer ${accessToken}` + const headers: AxiosRequestConfig["headers"] = { + ...(import.meta.server ? userAgentHeader : {}), + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), } + const axiosParams: AxiosRequestConfig = { baseURL: isVersioned ? `${baseUrl}v1/` : baseUrl, timeout: DEFAULT_REQUEST_TIMEOUT, diff --git a/frontend/test/README.md b/frontend/test/README.md index 66535e2c2ff..cdce4ec0ecf 100644 --- a/frontend/test/README.md +++ b/frontend/test/README.md @@ -5,7 +5,7 @@ locales and translation files to `i18n/locales` folder. To prevent too many requests during CI, we save the locale files that are necessary to run the app during Playwright tests. -1. `scripts/valid-locales.json` contains a list of valid locales with properties +1. `valid-locales.json` contains a list of valid locales with properties including their translation status (the percentage of strings that are translated). 2. `es.json` is the `es` locale file used for search navigation tests diff --git a/frontend/test/locales/ar.json b/frontend/test/locales/ar.json index 240d770c534..c253d24f3da 100644 --- a/frontend/test/locales/ar.json +++ b/frontend/test/locales/ar.json @@ -1,949 +1,561 @@ { - "404": { - "title": "يبدو أن المحتوى الذي تبحث عنه قد اختفى.", - "main": "انتقل إلى {link} أو ابحث عن شيء مشابه من الحقل أدناه." - }, - "hero": { - "subtitle": "استكشف أكثر من 800 مليون عمل إبداعي", - "description": "مكتبة واسعة من الصور المجانية ، والصور ، والصوت ، متاحة للاستخدام المجاني.", - "search": { - "placeholder": "البحث عن محتوى" - }, - "disclaimer": { - "content": "كل محتوى {openverse} يخضع لـ {license} أو في المجال العام.", - "license": "رخصة المشاع الإبداعي" - } - }, - "notification": { - "translation": { - "text": "ترجمة اللغة {locale} غير كاملة. ساعدنا في الوصول إلى 100 بالمائة عن طريق {link}.", - "link": "المساهمة بترجمة", - "close": "إغلاق مساهمة الترجمة يساعد على طلب الملصق" - }, - "analytics": { - "text": "المفرقعات تستخدم المحللين لتحسين نوعية خدمتنا زيارة {link} لتعلم المزيد أو اختيار.", - "link": "صفحة الخصوصية", - "close": "اغلق المحللين اللافتة" - } - }, - "header": { - "homeLink": "{openverse} البيت", - "placeholder": "البحث في كل المحتوى", - "aria": { - "primary": "الأولية", - "menu": "قائمة", - "search": "بحث", - "srSearch": "زر البحث" - }, - "aboutTab": "حول", - "resourcesTab": "موارد", - "loading": "جار التحميل...", - "filterButton": { - "simple": "مرشحات", - "withCount": "({count}) مرشح" - }, - "seeResults": "انظر النتائج", - "backButton": "عُد", - "contentSettingsButton": { - "simple": "Menu", - "withCount": "(مينو) {count} filter applied.Menu. {count}" - } - }, - "navigation": { - "about": "حول", - "licenses": "التراخيص", - "getInvolved": "شارك", - "api": "API", - "terms": "شروط", - "privacy": "خصوصية", - "feedback": "استجابة", - "sources": "مصادر", - "externalSources": "مصادر خارجية", - "searchHelp": "بحث في المساعدة" - }, - "about": { - "title": "حول {openverse}", - "description": { - "content": "{openverse} هي أداة تسمح باكتشاف أعمال المجال العام والمرخصة بشكل مفتوح واستخدامها من قبل الجميع." - }, - "collection": { - "content": { - "a": "{openverse} searches across more than 800 million images and audio from open APIs and the {commonCrawl} dataset.", - "b": "نحن نجمع العمل من عدة مستودعات عامة، ونسهل إعادة الاستخدام من خلال سمات مثل العزو البقعة الواحدة." - } - }, - "planning": { - "content": { - "a": "وحالياً، لا تقوم شركة {openverse} إلا بتفتيش الصور والصوت، والبحث عن الفيديو المقدم من مصادر خارجية.", - "b": "ونخطط لإضافة أنواع إضافية من وسائط الإعلام مثل النصوص المفتوحة ونماذج الـ 3 دال، بهدف نهائي هو إتاحة إمكانية الوصول إلى ما يقدر بـ 2.5 بليون من دولارات الولايات المتحدة المرخص لها والمجالات العامة على الشبكة.", - "c": "جميع رموزنا هي مصدر مفتوح ويمكن الوصول إليها في {working} نحن {repository} يمكنك أن ترى ما هو {community}" - }, - "repository": "مستودع {openverse} {github}", - "community": "نرحب بمساهمة المجتمع", - "working": "نحن نعمل حاليا على" - }, - "transfer": { - "content": { - "a": "{openverse} is the successor to CC search which was launched by Creative Commons in 2019, after its migration to WordPress in 2021.", - "b": "يمكنك قراءة المزيد عن هذا الانتقال في الإعلانات الرسمية من {creativeCommons} and {wordpress}", - "c": "ولا نزال ملتزمين بتحقيق هدفنا المتمثل في معالجة إمكانية اكتشاف وسائل الإعلام المفتوحة والوصول إليها." - } - }, - "declaration": { - "content": { - "a": "{openverse} does not verify licensing information for individual works, or whether the generated attribution is accurate or complete.", - "b": "يرجى التحقق بشكل مستقل من حالة الترخيص والمعلومات المتعلقة بالإسناد قبل إعادة استخدام المحتوى. للحصول على المزيد من التفاصيل، قراءة {terms}" - }, - "terms": "{openverse} شروط الاستخدام" - } - }, - "sources": { - "title": "مصادر", - "detail": "يتيح لك النقر فوق {singleName} تصفح العناصر الموجودة داخل هذا المصدر وتصفيتها.", - "singleName": "مصدر", - "providers": { - "source": "مصدر", - "domain": "اِختِصاص", - "item": "مجموع العناصر" - }, - "ccContent": { - "where": "من أين يأتي المحتوى على {openverse}؟", - "content": "هناك محتوى مرخص بشكل علني مستضاف على ملايين المجالات عبر اتساع الإنترنت. يحدد فريقنا بشكل منهجي مقدمي الخدمة الذين يستضيفون محتوى مرخصًا بـ CC. إذا كان ملائمًا ، فنحن نفهرس هذا المحتوى ونجعله قابلاً للاكتشاف من خلال {openverse}.", - "provider": { - "a": "ولدى بعض مقدمي الخدمات تجمعات متعددة مختلفة من المحتوى داخلهم. {flickr} has sources ranging from NASA to personal photography. The {smithsonian} comprises a dozen diverse collections.", - "b": "ويدير مشاعات الويكيو القفازات من حيث المحتوى، ويستخدمها عدد من المجرات والمكتبات والمحفوظات والمتاحف التي تسلط الضوء على بعض أو كل مجموعاتها الرقمية." - }, - "europeana": "{openverse} ممتن بشكل خاص لعمل {link} ، وهي منظمة تعمل على رقمنة وإنشاء أعمال التراث الثقافي القابلة للاكتشاف في جميع أنحاء أوروبا. {openverse} قادر على فهرسة مئات المصادر القيمة من خلال تكامل واحد مع {linkApi}." - }, - "newContent": { - "next": "كيف نقرر ما هي المصادر التي يجب إضافتها بعد ذلك؟", - "integrate": "لدينا قائمة لا تنتهي أبدًا بالمصادر المحتملة للبحث قبل التكامل. نسأل أنفسنا أسئلة مثل:", - "impact": " ما هو تأثير أو أهمية هذا المصدر لمستخدمينا؟ إذا كان موجودًا داخل مزود مثل ويكيميديا كومنز ، فهل من المفيد لمستخدمينا أن يكونوا قادرين على التصفية حسب هذا المصدر مباشرة؟", - "reuse": "هل معلومات الترخيص والإسناد معروضة بوضوح لتمكين إعادة الاستخدام الواثق؟", - "totalItems": "كم عدد العناصر الإجمالية الجديدة أو الأنواع الجديدة من العناصر التي يمكننا تقديمها لمستخدمينا من خلال هذا التكامل؟ بعض المصادر عبارة عن تكاملات مباشرة ، بينما قد يكون البعض الآخر مصدرًا داخل المصدر." - }, - "suggestions": "نحن نقدر الاقتراحات لمصادر جديدة من مجتمع المستخدمين لدينا.", - "issueButton": "اقترح مصدر جديد", - "aria": { - "table": "جدول المصادر" - }, - "heading": { - "image": "مصادر الصور", - "audio": "مصادر الصوت" - } - }, - "externalSourcesPage": { - "title": "بحث ميتا", - "intro": "تم إنشاء {openverse} فوق كتالوج يقوم بفهرسة المحتوى المرخص لـ CC والمحتوى العام من مصادر محددة. تعرف على المزيد حول {link} الخاص بنا.", - "link": "مصادر", - "license": { - "a": "ومع ذلك، هناك مصادر عديدة لوسائط الإعلام المرخص بها من قبل لجنة مكافحة الإرهاب ووسائط الإعلام العامة التي لم نتمكن بعد من إدراجها في بحث {openverse}.", - "b": "This might be because they do not offer a public API, or that our contributors have not yet had time to integrate them into {openverse}", - "c": "هذه مصادر قيّمة و نريد أن نتأكّد من أنّك قادر على إيجاد أفضل المواد المرخصة علناً، بغض النظر عن مكان وجودها." - }, - "new": { - "title": "هل يمكنني اقتراح مصادر خارجية جديدة؟", - "content": "نعم من فضلك! أنشئ {issue} في مستودع GitHub أو أرسل إلينا {email} وأخبرنا بالمصادر الجديدة التي ترغب في تضمينها.", - "issue": "القضية", - "email": "البريد الإلكتروني" - }, - "why": { - "title": "لماذا بنيت هذا؟", - "content": "لسنوات عديدة ، قدمت Creative Commons لمستخدميها بوابة بحث مخصصة لمنصات البحث التي تحتوي على عوامل تصفية ترخيص CC مضمنة. في الواقع ، لا يزال هذا قيد الصيانة في {old}.", - "new": { - "a": "بالنسبة لمستخدمي موقع البحث عن الميثان، سيبدو سمة " المصادر الخارجية " في {openverse} مألوفة.", - "b": "وكان الهدف هو ضمان عدم فقدان القدرة الوظيفية، ولكن يجري تحديثها وإدراجها في محرك البحث الجديد الخاص بمحتواها المرخص بشكل مفتوح.", - "c": "وبالإضافة إلى ذلك، تستند سمة " المصادر الخارجية " إلى هذه الوظيفة، مما يتيح لنا أن نضيف بسرعة مصادر خارجية جديدة عند اكتشافها، وأن ندعم أنواع المحتوى الجديدة في المستقبل." - }, - "ariaLabel": "استجابة", - "feedbackSuggestions": "نأمل أن تستمتع ، وإذا كانت لديك اقتراحات للتحسين ، فاترك لنا {feedback}.", - "feedbackLink": "استجابة" - }, - "relationships": { - "a": "وهذه الوظيفة تتيح لنا أيضا البدء في المحادثات وبناء علاقات مع المصادر التي قد ترغب في إدراجها في قضية {openverse} في المستقبل.", - "b": "وأخيرا، يمكننا أيضا أن نعرض مصادر خارجية لأنواع وسائط الإعلام التي لا ندرجها في {openverse}، ولكن نخطط لذلك." - }, - "explanation": "يمكنك العثور على روابط لمصادر خارجية أسفل كل صفحة نتائج بحث {openverse} ؛ في صفحات عمليات البحث التي لا تظهر نتائج ؛ وعلى صفحات أنواع الوسائط التي لا ندعمها بعد ولكننا نعتزم ذلك." - }, - "privacy": { - "title": "خصوصية", - "intro": { - "content": "يسعى مشروع {openverse} إلى جعل خصوصية مستخدمينا وأمانهم أولوية. {openverse} تلتزم بـ {link}. يرجى الاطلاع على هذا المستند للحصول على وصف كامل لكيفية استخدام {openverse} وحمايته لأي معلومات تزودنا بها.", - "link": "سياسة الخصوصية لجميع مواقع WordPress.org" - }, - "cookies": { - "title": "ملفات تعريف الارتباط", - "content": { - "a": "{openverse} تستخدم البسكويت لتخزين المعلومات عن أفضليات الزائرين ومعلومات عن مروجهم على الشبكة. ونحن نستخدم هذه المعلومات لتحسين خبرة مستخدمي الموقع.", - "b": "هذه تعتبر \"الوكالة\" أو \"الكوكيز الضروري جداً\" قد تبطلين هذه من خلال تغيير أطرك للطوارئ، ولكن هذا قد يؤثر على كيفية وظائف {openverse}" - } - }, - "contact": { - "title": "اتصل بنا", - "content": "يمكن إرسال أي أسئلة حول {openverse} والخصوصية إلى {email} ، أو مشاركتها على أنها {issue} ، أو مناقشتها مع مجتمعنا في #openverse قناة {chat}.", - "issue": "مشكلة GitHub", - "chat": "جعل WordPress دردشة" - } - }, - "searchGuide": { - "title": "{openverse} دليل البنية", - "intro": "عند البحث ، يمكنك إدخال رموز أو كلمات خاصة لمصطلح البحث الخاص بك لجعل نتائج البحث أكثر دقة.", - "exact": { - "title": "ابحث عن تطابق تام", - "ariaLabel": "اقتبس اقتباس كلود مونيه", - "claudeMonet": "\"كلود مونيه\"", - "content": "ضع كلمة أو عبارة داخل علامات الاقتباس. على سبيل المثال ، {link}." - }, - "negate": { - "title": "المصطلحات المستبعدة", - "operatorName": "ناقصا", - "ariaLabel": "الكلب مطروحا منه", - "example": "كلب", - "content": "لاستبعاد مصطلح من نتائجك، وضع {operator} أمامه. Example: {link}{br} هذا سيبحث عن وسائل الإعلام ذات الصلة بـ\"الكلب\" لكن لن يتضمن النتائج ذات الصلة بـ \"بوغ\"" - } - }, - "feedback": { - "title": "استجابة", - "intro": "شكرًا لك على استخدام {openverse}! نرحب بأفكارك لتحسين الأداة أدناه. لتقديم ملاحظات منتظمة ، انضم إلى قناة {slack} في مساحة عمل {makingWordpress} Slack.", - "improve": "ساعدنا لنتحسن", - "report": "الإبلاغ عن خطأ", - "loading": "جار التحميل...", - "aria": { - "improve": "ساعدنا في تحسين الشكل", - "report": "الإبلاغ عن نموذج خطأ" - } - }, - "sensitive": { - "title": "المحتوى الحساس", - "description": { - "content": { - "a": "{openverse} operates along a “safe-default” approach in all aspects of its operation and development, with the intention of being as inclusive and accessible as possible.", - "b": "ومن ثم، فإن {openverse} لا تشمل سوى النتائج ذات المحتوى الحساس عندما اختار المستعملون صراحة " تحقيق نتائج حساسة " بشأن {openverseOrg} وفي {openverse} API.", - "c": "In adherence to {wpCoc} and its {deiStatement}, {openverse} holds contributors to high expectations regarding conduct towards other contributors, the accessibility of contribution and the services, and, therefore, being an inclusive project.", - "d": "Similarly, {openverse} holds the expectation that the results returned from the API or displayed on the {openverseOrg} should be accessible by default.", - "e": "على الجميع، بغض النظر عن خلفيتهم، أن يشعروا بالأمان، وأنهم مشمولون في {openverse}، سواء كانوا مساهمين في الجوانب التقنية لخدمات {openverse}، وهو مبدع مشمول بأعماله في {openverse}", - "f": "{openverse} recognises its responsibility as a tool used by people of a wide variety of ages, including young people in educational settings, and pays particular attention to minimizing accidental interaction with or exposure to sensitive content." - }, - "wpCoc": "مدونة قواعد السلوك المجتمعية", - "deiStatement": "التنوع والإنصاف وبيان الإدماج" - }, - "sensitivity": { - "what": { - "a": "{openverse} تستخدم مصطلح \"حساس\" بدلاً من \"المكانة\" أو \"الخدمة العامة\" (غير آمنة للعمل) أو مصطلحات أخرى للإشارة إلى أن تعريفنا للمحتوى حساس هو واسع، مع التركيز على إمكانية الوصول والإدماج.", - "b": "وهذا يعني أن بعض المحتويات تُسمّى بـ \"حساسية\" التي لا تندرج في فئة مما يُفهم عموماً على أنها محتوى \"المكانة\" (أي بعبارة أخرى، المحتوى تحديداً لجمهور بالغ).", - "c": "غير أن التسمية لا تعني ضمناً أن {openverse} أو متعهديها يعتبرون المضمون غير ملائم للمنبر بشكل عام، وهو أيضاً لا ينطوي على حكم أخلاقي أو أخلاقي.", - "d": "نحن نعتبر المحتوى \"الحساس\" مُحتوى مُهين أو مُزعج، أو غير لائق، مع إيلاء اهتمام خاص للشباب." - }, - "how": { - "a": "ويتسم تعريف الحساسية هذا بدرجة كبيرة من المرونة وهو غير دقيق عن قصد.", - "b": "وتعتمد شركة {openverse} على مجموعة متنوعة من الأدوات لاكتشاف المحتوى الذي يمكن أن يكون حساسا، بما في ذلك تقارير معتدلة للمستعملين عن العمل الفردي ومسح المحتوى النصي المتصل بالعمل ذي الحساسية.", - "c": "ويرد أدناه وصف أكثر تفصيلا لهذه المسائل." - } - }, - "onOff": { - "title": "تغيير المحتوى الحساس", - "sensitiveResults": "بالخطأ {openverse} does not include sensitive content in search results. ويتطلب إدراج النتائج الحساسة اختيارا صريحا من المستخدم. ويمكن للمستعمل أن يختار إدراج المحتوى الحساس في نتائج البحث بتمكين مفتاح " النتائج الحسية " .", - "blurSensitive": { - "a": "وعندما يُدرج المحتوى الحساس، تكون النتائج الحساسة التي تُعاد غير واضحة أيضاً لمنع التعرض العرضي.", - "b": "كما أن عدم طمسها يتطلب اختيارا صريحا من المستخدم. ويمكن للمستعمل أن يختار أن يرى محتوى حساسا غير واضح من خلال تبديل مفتاح " محتوى اللغم " ." - }, - "where": "وكلاهما متاحان في قاذفة التصفية (على الحواسيب المكتبية) وفي شريط " فليتر " لأماكن البحث (على الأجهزة المحمولة) على صفحة نتائج البحث." - }, - "designations": { - "title": "تعيينات المحتوى الحساسة", - "description": { - "a": "{openverse} يعيّن محتوىً حساساً في API وفي موقع {openverseOrg} على شبكة الإنترنت باستخدام طريقتين: تقارير من مستعملي {openverse} واكتشاف النصوص الحساسة آلياً.", - "b": "ولا تقتصر هذه التسميات على بعضها البعض، ويمكن أن يطبق عليها عمل واحد أو كليهما." - }, - "userReported": { - "title": "المستعمل أبلغ عن حساسية", - "description": { - "a": "{openverse} users are invited to report sensitive content via the {openverseOrg} website and the {openverse} API.", - "b": "Some tools and apps that integrate with the {openverse} API, like the {gutenbergMediaInserter}, also allow their users to report sensitive content.", - "c": "وتشمل صفحة العمل الفردي القدرة على الإبلاغ عن المحتوى على أنه حساس (أو الإبلاغ عن انتهاكات الحقوق).", - "d": "وتحقق من هذه التقارير ويتخذون قرارات بشأن ما إذا كان ينبغي إضافة تسمية حساسية للعمل، أو في بعض الحالات، على النحو المبين أعلاه، شطب العمل من مكتب المدعي العام." - }, - "gutenbergMediaInserter": "Gutenberg editor’s {openverse}" - }, - "sensitiveText": { - "title": "المحتوى النصي الحساس", - "description": { - "a": "{openverse} يمسح بعض البيانات الوصفية النصية المتصلة بالأعمال التي توفرها مصادرنا بشروط حساسة.", - "b": "{openverse} {sensitiveTermsList} is open source and contributions and input from the community are welcome and invited.", - "c": "ومن أمثلة النصوص التي يمكن أن تكون حساسة النص، على سبيل المثال لا الحصر، نصوص ذات طابع جنسي أو بيولوجي أو عنيف أو عنصري أو غير ذلك من أشكال عدم التقيد.", - "d": "The project recognises that this approach is imperfect and that some works may inadvertently receive a sensitivity nomination without necessarily being sensitive.", - "e": "ولمزيد من السياق بشأن السبب الذي جعلنا نختار هذا النهج على الرغم من ذلك، نشير إلى {imperfect} من وثيقة تخطيط مشاريعنا المتصلة بهذه السمة." - }, - "sensitiveTermsList": "قائمة المصطلحات الحساسة", - "imperfect": "قسم شرطة لوس أنجلوس", - "metadata": { - "a": "It is important to note that some textual metadata for a work is {notAvailable} من خلال موقع {openverse} API أو موقع {openverseOrg}", - "b": "غير أن هذه البيانات الوصفية لا تزال مقصودة بشروط حساسة ولا تعامل كحالة خاصة.", - "c": "وفي حالة ما إذا وجد نص {openverse} شروطا حساسة في حقول البيانات الفوقية هذه للعمل، فإن العمل سيظل يتلقى تعيينا حساسا يستند إلى نص حساس حتى وإن لم يكن النص الحساس نفسه متاحا عن طريق {openverse}", - "d": "وتأخذ شركة {openverse} النهج القائل بأن المحتوى النصي الحساس في وصف ما هو مؤشر عال نسبيا من مؤشرات الأعمال الحساسة المحتملة.", - "e": "As above, {openverse} understands that this is not perfect." - }, - "notAvailable": "غير متاحة" - } - }, - "faq": { - "title": "الأسئلة المتكررة", - "one": { - "question": "وقد وجدت المحتوى الذي أعتقد أنه حساس لا يوجد فيه تحديد حساسية. ماذا يجب أن أفعل؟", - "answer": { - "a": "يرجى الإبلاغ عن المحتوى الحساس عن طريق زيارة صفحة العمل الفردي على موقع {openverseOrg} على شبكة الإنترنت، واستخدام زر " إعادة الإبلاغ عن هذا المحتوى " تحت معلومات الإسناد وفوق البطاقات.", - "b": "{openverse}" - } - }, - "two": { - "question": "لا أوافق على تحديد الحساسية في العمل هل يمكنك أن تزيله من فضلك؟", - "answerPt1": "بالنسبة للتسميات القائمة على النصوص، {openverse} does not at the moment have a method for removing the nomination. This is a feature that will be built eventually, but is not part of the baseline sensitive content detection feature.", - "answerPt2": { - "a": "وفيما يتعلق بالتسميات التي أبلغ عنها المستعمل، يرجى تقديم تقرير جديد عن صفحة العمل بعد التعليمات الواردة في السؤال السابق.", - "b": "في الملاحظات، وصف لماذا تعتقد أن العمل لا ينبغي أن يكون له تحديد حساسية.", - "c": "As when added a new nomination, {openverse} reserves the right to respectfully decline the request to remove a confirmed user sensitivity nomination." - } - }, - "three": { - "question": "وقد وجدت محتوى في {openverse} قد يكون غير قانوني. إلى جانب إبلاغه إلى {openverse}، هل هناك أي خطوات أخرى يمكنني اتخاذها؟", - "answer": { - "a": "وفيما يتعلق بالتسميات التي أبلغ عنها المستعمل، يرجى تقديم تقرير جديد عن صفحة العمل بعد التعليمات الواردة في السؤال السابق.", - "b": "في الملاحظات، وصف لماذا تعتقد أن العمل لا ينبغي أن يكون له تحديد حساسية.", - "c": "As when added a new nomination, {openverse} reserves the right to respectfully decline the request to remove a confirmed user sensitivity nomination." - } - } - } - }, - "tags": { - "title": "Understanding Tags in {openverse}", - "intro": { - "a": "وقد يكون لكل عمل إبداعي في {openverse} بطاقات، ومجموعة اختيارية من الكلمات الرئيسية المستخدمة لوصف العمل وجعل من الأسهل للمستعملين أن يجدوا وسائل الإعلام ذات الصلة للبحث عنهم.", - "b": "وتنقسم هذه البطاقات إلى فئتين رئيسيتين: علامات المصدر والعلامات المولدة. فهم الفرق بينهما يمكن أن يعزز خبرتك في البحث ويحسن دقة نتائجك" - }, - "sourceTags": { - "title": "المصدر", - "content": { - "a": "وعلامات المصدر هي علامات نشأت من المصدر الأصلي للعمل الإبداعي. ويمكن أن يضاف المساهمون المختلفون هذه البطاقات، مثل المصور الذي قام بتحميل صورتهم إلى فليكر والعلامات الوصفية الإضافية.", - "b": "ويمكن للمنبر الأصلي نفسه أن يخصص بطاقات إضافية من أعضاء المجتمع المحلي أو التشغيل الآلي أو مصادر أخرى." - } - }, - "generatedTags": { - "title": "تاغس المولدة", - "content": { - "a": "وتُنشأ العلامات المولدة من خلال التحليل الآلي للأشغال الإبداعية، ومعظم الصور الشائعة. وتنطوي هذه العملية على تكنولوجيات متطورة مثل AWS Rekognition ، وكلاريفاي، وغير ذلك من خدمات التعرف على الصور التي تحلل المحتوى وتولد علامات وصفية.", - "b": "وفي حين أن النظم الآلية الموثوق بها عموما يمكن أن تسيئ أحيانا تفسير أو تفوت عناصر في صورة ما.", - "c": "وتبذل الجهود لاستبعاد أي علامات متولدة تشير إلى هويات أو انتماءات البشر.", - "d": "إذا واجهتم أي صور بعلامات متولدة تضعون افتراضات حول، على سبيل المثال، الجنس أو الدين أو الانتماء السياسي، يرجى الإبلاغ عن الصور باستخدام زر \" التقرير \" على صفحات النتائج الوحيدة." - } - } - }, - "error": { - "occurred": "حدث خطأ", - "imageNotFound": "تعذر العثور على الصورة بالمعرف {id}", - "mediaNotFound": "تعذر العثور على {mediaType} بالمعرف {id}", - "image": "صورة", - "audio": "صوتي" - }, - "filters": { - "title": "مرشحات", - "filterBy": "مصنف بواسطة", - "licenses": { - "title": "التراخيص", - "cc0": "CC0", - "pdm": "علامة المجال العام", - "by": "بواسطة", - "bySa": "BY-SA", - "byNc": "BY-NC", - "byNd": "BY-ND", - "byNcSa": "BY-NC-SA", - "byNcNd": "BY-NC-ND" - }, - "licenseTypes": { - "title": "يستخدم", - "commercial": "استخدام تجاريًا", - "modification": "تعديل أو تكييف" - }, - "imageProviders": { - "title": "مصدر" - }, - "audioProviders": { - "title": "مصدر" - }, - "audioCategories": { - "title": "فئة الصوت", - "audiobook": "كتاب مسموع", - "music": "موسيقى", - "news": "أخبار", - "podcast": "تدوين صوتي", - "pronunciation": "النطق", - "sound_effect": "مؤثرات صوتية", - "sound": "مؤثرات صوتية" - }, - "imageCategories": { - "title": "نوع الصورة", - "photograph": "الصور", - "illustration": "الرسوم التوضيحية", - "digitized_artwork": "الأعمال الفنية الرقمية" - }, - "audioExtensions": { - "title": "امتداد", - "flac": "FLAC", - "mid": "منتصف", - "mp3": "MP3", - "oga": "OGA", - "ogg": "OGG", - "opus": "OPUS", - "wav": "WAV", - "webm": "WEBM" - }, - "imageExtensions": { - "title": "امتداد", - "jpg": "JPEG", - "png": "بي إن جي", - "gif": "GIF", - "svg": "SVG" - }, - "aspectRatios": { - "title": "ابعاد متزنة", - "tall": "طويل", - "wide": "واسع", - "square": "ميدان" - }, - "sizes": { - "title": "حجم الصورة", - "small": "صغير", - "medium": "متوسط", - "large": "كبير" - }, - "safeBrowsing": { - "title": "الحشد الآمن", - "desc": "ولا يظهر التخلف المضمون الذي يُعرف باسم {sensitive}..", - "sensitive": "حساسة", - "toggles": { - "fetchSensitive": { - "title": "النتائج الحساسة", - "desc": "واتسمت النتائج بالحساسية في مجال النتائج." - }, - "blurSensitive": { - "title": "محتوى البق", - "desc": "الصور والنصوص لمنع رؤية المواد الحساسة" - } - } - }, - "lengths": { - "title": "مدة", - "shortest": "<30 ثانية", - "short": "30 ثانية -2 دقيقة", - "medium": "2-10 دقائق", - "long": "> 10 دقائق" - }, - "creator": { - "title": "البحث حسب المنشئ" - }, - "searchBy": { - "title": "البحث عن طريق", - "creator": "المنشئ" - }, - "licenseExplanation": { - "licenseDefinition": "تعريف الترخيص", - "markDefinition": "تعريف {mark}", - "more": { - "license": "{readMore} حول هذا الترخيص.", - "mark": "{readMore} حول {mark}.", - "readMore": "اقرأ أكثر" - } - }, - "aria": { - "removeFilter": "إزالة مرشح {label}" - } - }, - "filterList": { - "filterBy": "مصنف بواسطة", - "hide": "إخفاء عوامل التصفية", - "clear": "مسح عوامل التصفية", - "clearNumbered": "مرشحات واضحة ({number})", - "show": "أظهر النتائج", - "categoryAria": "قائمة عوامل التصفية لفئة {categoryName}" - }, - "browsePage": { - "allNoResults": "لا نتائج", - "allResultCount": "{localeCount} نتيجة | {localeCount} نتيجة", - "allResultCountMore": "أكثر من {localeCount} نتيجة | أكثر من {localeCount} نتيجة", - "contentLink": { - "image": { - "zero": "لا توجد صور لـ {query}", - "count": "See {localeCount} image found for {query}.|See {localeCount} images found for {query}.", - "countMore": "انظر على صور {query} {localeCount}" - }, - "audio": { - "zero": "لم يعثر على أي صوت لـ {query}.", - "count": "انظر {localeCount} {query}", - "countMore": "انظر فوق {query} {localeCount}" - } - }, - "load": "تحميل المزيد من النتائج", - "loading": "جار التحميل...", - "fetchingError": "خطأ في جلب {type}:", - "searchRating": { - "content": "هل هذه النتائج ذات صلة؟", - "yes": "نعم", - "no": "رقم", - "feedbackThanks": "شكرا لملاحظاتك!" - }, - "searchForm": { - "placeholder": "بحث في كل {type}", - "image": "الصور", - "audio": "صوتي", - "video": "أشرطة فيديو", - "model3d": "نماذج ثلاثية الأبعاد", - "all": "المحتوى", - "collectionPlaceholder": "بحث في هذه المجموعة", - "button": "يبحث", - "clear": "آمن" - }, - "licenseDescription": { - "title": "رخصة CC", - "by": "ائتمن الخالق.", - "nc": "الاستخدامات غير التجارية فقط.", - "nd": "لا مشتقات أو تعديلات مسموح بها.", - "sa": "شارك التعديلات تحت نفس الشروط.", - "zero": "تم وضع علامة على هذا العمل على أنه مخصص للملك العام.", - "pd": "تم وضع علامة على هذا العمل على أنه في المجال العام.", - "samplingPlus": "العينات والمزج والتحولات الإبداعية مسموح بها." - }, - "aria": { - "close": "أغلق", - "scroll": "انتقل إلى أعلى", - "search": "بحث", - "removeFilter": "إزالة عامل التصفية", - "licenseExplanation": "شرح الترخيص", - "creator": "البحث عن طريق المنشئ", - "imageTitle": "Image: {title}", - "audioTitle": "Audio: {title}", - "resultsLabel": { - "all": "جميع النتائج لـ {query}", - "image": "Image results for {query}", - "audio": "Audio tracks for {query}" - }, - "results": { - "all": "جميع النتائج لـ \"{query}\" و{imageResults} و{audioResults}.", - "image": { - "zero": "لا توجد نتائج للصور لـ \"{query}\"", - "count": "{localeCount} {query}", - "countMore": "أهم نتائج الصور {localeCount} لـ \"{query}\"." - }, - "audio": { - "zero": "لا توجد آثار صوتية لـ \"{query}\"", - "count": "{localeCount} {query}", - "countMore": "أهم المقاطع الصوتية {localeCount} لـ \"{query}\"." - } - }, - "allResultsHeadingCount": { - "image": { - "zero": "لا صور", - "count": "{localeCount} image.{localeCount} images", - "countMore": "أعلى صور {localeCount}" - }, - "audio": { - "zero": "لا آثار صوتية", - "count": "{localeCount} {localeCount}", - "countMore": "أعلى {localeCount}" - } - } - } - }, - "mediaDetails": { - "information": { - "type": "يكتب", - "unknown": "مجهول", - "category": "فئة" - }, - "scroll": { - "forward": "تقدم للأمام", - "back": "تراجع" - }, - "reuse": { - "title": "كيف تستعمل", - "description": "قم بزيارة موقع ويب {media} لتنزيله واستخدامه. تأكد من أن تنسب الفضل إلى المنشئ من خلال إظهار معلومات الإسناد حيث تشارك عملك.", - "copyrightDisclaimer": "قد تحتوي بعض الصور الفوتوغرافية على محتوى محمي بحقوق الطبع والنشر، مثل اللوحات أو المنحوتات أو الأعمال المعمارية. قد يتطلب استخدام هذه الصور الحصول على أذونات إضافية من صاحب حقوق الطبع والنشر للأعمال المصورة.", - "licenseHeader": "رخصة", - "toolHeader": "المجال العام", - "audio": "صوتي", - "image": "صورة", - "tool": { - "content": "اقرأ المزيد عن الأداة {link}.", - "link": "هنا" - }, - "credit": { - "genericTitle": "هذا العمل", - "actualTitle": "\"{title}\"", - "text": "{title} {creator} {markedLicensed} {license}. {viewLegal}", - "creatorText": "بواسطة {creatorName}", - "marked": "تم وضع علامة", - "licensed": "مرخص بموجب", - "viewLegalText": "لعرض {termsCopy} ، قم بزيارة {url}.", - "termsText": "الشروط", - "copyText": "نسخة من هذا الترخيص" - }, - "copyLicense": { - "title": "انسب الفضل إلى المنشئ", - "rich": "النص الغني", - "html": "لغة البرمجة", - "plain": "نص عادي", - "copyText": "نسخ النص", - "copied": "نسخ!", - "xml": "إك سم إل" - }, - "attribution": "تم تمييز هذه الصورة بترخيص {link}:" - }, - "providerLabel": "مزود", - "sourceLabel": "مصدر", - "providerDescription": "الموقع الشبكي الذي يستضيف فيه المحتوى", - "sourceDescription": "المنظمة التي أنشأت أو تملك المحتوى الأصلي", - "loading": "جار التحميل...", - "relatedError": "خطأ في جلب الوسائط ذات الصلة", - "aria": { - "attribution": { - "license": "اقرأ المزيد عن الترخيص", - "tool": "اقرأ المزيد عن الأداة" - }, - "creatorUrl": "اسم {creator}" - }, - "imageInfo": "معلومات الصورة", - "audioInfo": "معلومات صوتية", - "tags": { - "title": "العلامات", - "generated": { - "heading": "العلامات المولدة", - "pageTitle": "تعلم المزيد" - }, - "source": { - "heading": "بطاقات المصدر" - }, - "showMore": "أظهر المزيد", - "showLess": "تظهر أقل" - }, - "contentReport": { - "short": "تقرير", - "long": "الإبلاغ عن هذا المحتوى", - "form": { - "disclaimer": "لأغراض أمنية ، يجمع {openverse} ويحتفظ بعناوين IP مجهولة المصدر لأولئك الذين يكملون هذا النموذج ويرسلونه.", - "question": "ماهو السبب؟", - "dmca": { - "option": "ينتهك حقوق الطبع والنشر", - "note": "يجب عليك ملء هذا {form} للإبلاغ عن انتهاك حقوق النشر. لن يتم اتخاذ أي إجراء حتى يتم ملء هذا النموذج وإرساله. نوصي بعمل الشيء نفسه في المصدر ، {source}.", - "form": "نموذج DMCA", - "open": "فتح النموذج" - }, - "sensitive": { - "option": "يحتوي على محتوى حساس", - "subLabel": "اختياري", - "placeholder": "اختياريًا ، قدم وصفًا." - }, - "other": { - "option": "آخر", - "note": "صف القضية.", - "subLabel": "مطلوب", - "placeholder": "الرجاء إدخال 20 حرفًا على الأقل." - }, - "submit": "تقرير", - "cancel": "يلغي" - }, - "success": { - "title": "تم إرسال التقرير بنجاح", - "note": "شكرا على الإبلاغ هذا المحتوى. نوصي بعمل الشيء نفسه في المصدر ، {source}." - }, - "failure": { - "title": "تعذر تقديم التقرير", - "note": "حدث خطأ ما ، يرجى المحاولة مرة أخرى بعد قليل." - } - } - }, - "singleResult": { - "back": "رجوع إلى نتائج البحث" - }, - "imageDetails": { - "creator": "بالاسم{name}", - "weblink": "احصل على هذه الصورة", - "information": { - "dimensions": "أبعاد", - "pixels": "بكسل", - "sizeInPixels": "{width} × {height} بكسل" - }, - "relatedImages": "الصور ذات الصلة", - "aria": { - "creatorUrl": "المؤلف {creator}" - } - }, - "audioDetails": { - "genreLabel": "النوع", - "relatedAudios": "الصوت ذو الصلة", - "table": { - "album": "الألبوم", - "sampleRate": "معدل العينة", - "filetype": "شكل", - "genre": "النوع" - }, - "weblink": "احصل على هذا الصوت" - }, - "allResults": { - "snackbar": { - "text": "Press {spacebar} to play or pause the track.", - "spacebar": "Spacebar" - } - }, - "audioResults": { - "snackbar": { - "text": "Press {spacebar} to play or pause, and {left} "{right} to seek through the track.", - "spacebar": "Spacebar", - "left": "♪", - "right": "?" - } - }, - "externalSources": { - "caption": "{openverse} لا يتحكم في النتائج ، تحقق دائمًا من أن العمل في الواقع خاضع لترخيص CC.", - "button": "قائمة مصدر", - "title": "مصادر خارجية", - "card": { - "search": "لا تجد ما تبحث عنه؟ جرب المصادر الخارجية", - "caption": "انقر فوق أحد المصادر أدناه للبحث مباشرة في مجموعات أخرى من الصور المرخصة CC. {break} يرجى ملاحظة أن استخدام المرشحات غير مدعوم لمكتبة Open Clip Art Library أو Nappy." - }, - "form": { - "supportedTitle": "لا تجد ما تبحث عنه؟ ابحث في مصادر خارجية", - "supportedTitleSm": "ابحث في مصادر خارجية" - } - }, - "browsers": { - "chrome": "كروم", - "firefox": "ثعلب النار", - "opera": "Opera", - "edge": "حافة" - }, - "waveform": { - "label": "شريط البحث عن الصوت", - "currentTime": "{time} ثانية | {time} ثانية" - }, - "audioThumbnail": { - "alt": "صورة الغلاف لـ \"{title}\" بواسطة {creator}" - }, - "audioTrack": { - "ariaLabel": "{title} - مشغل الصوت", - "ariaLabelInteractive": "{title} - مشغل الصوت - اضغط على مفتاح المسافة لتشغيل معاينة الصوت وإيقافها مؤقتًا", - "ariaLabelInteractiveSeekable": "Audio: {title} - اللاعب التفاعلي - اضغط على حانة الفضاء لتلعب وتتوقف عن النظرة المسبقة للصوت؛ استخدام مفاتيح السهم اليسرى واليمن للبحث عن طريق المسار.", - "messages": { - "err_aborted": "لقد أجهضت التشغيل.", - "err_network": "حدث خطأ في الشبكة.", - "err_decode": "تعذر فك تشفير الصوت.", - "err_unallowed": "لا يسمح بالاستنساخ.", - "err_unknown": "حدث خطأ غير متوقع حاول مرة أخرى في بضع دقائق أو الإبلاغ عن البند إذا استمرت المسألة.", - "err_unsupported": "لا يدعم متصفحك تنسيق الصوت هذا.", - "loading": "جار التحميل..." - }, - "creator": "بواسطة {creator}", - "close": "أغلق مشغل الصوت" - }, - "playPause": { - "play": "لعب", - "pause": "يوقف", - "replay": "إعادة", - "loading": "جار التحميل" - }, - "search": { - "search": "يبحث", - "searchBarLabel": "البحث عن محتوى في {openverse}" - }, - "licenseReadableNames": { - "cc0": "صفر", - "pdm": "علامة المجال العام", - "by": "الإسناد", - "bySa": "نَسب المُصنَّف - شارك - على حدٍ سواء", - "byNc": "نَسب المُصنَّف - غير تجاري", - "byNd": "Attribution-NoDerivatives", - "byNcSa": "Attribution-NonCommercial-Share-Alike", - "byNcNd": "نسب المصنف غير التجاري لا المشتقات", - "sampling+": "سامبلينغ بلس", - "ncSampling+": "أخذ العينات غير التجارية زائد" - }, + "404.title": "يبدو أن المحتوى الذي تبحث عنه قد اختفى.", + "404.main": "انتقل إلى {link} أو ابحث عن شيء مشابه من الحقل أدناه.", + "hero.subtitle": "استكشف أكثر من 800 مليون عمل إبداعي", + "hero.description": "مكتبة واسعة من الصور المجانية ، والصور ، والصوت ، متاحة للاستخدام المجاني.", + "hero.search.placeholder": "البحث عن محتوى", + "hero.disclaimer.content": "كل محتوى {openverse} يخضع لـ {license} أو في المجال العام.", + "hero.disclaimer.license": "رخصة المشاع الإبداعي", + "notification.translation.text": "ترجمة اللغة {locale} غير كاملة. ساعدنا في الوصول إلى 100 بالمائة عن طريق {link}.", + "notification.translation.link": "المساهمة بترجمة", + "notification.translation.close": "إغلاق مساهمة الترجمة يساعد على طلب الملصق", + "notification.analytics.text": "المفرقعات تستخدم المحللين لتحسين نوعية خدمتنا زيارة {link} لتعلم المزيد أو اختيار.", + "notification.analytics.link": "صفحة الخصوصية", + "notification.analytics.close": "اغلق المحللين اللافتة", + "header.homeLink": "{openverse} البيت", + "header.placeholder": "البحث في كل المحتوى", + "header.aria.primary": "الأولية", + "header.aria.menu": "قائمة", + "header.aria.search": "بحث", + "header.aria.srSearch": "زر البحث", + "header.aboutTab": "حول", + "header.resourcesTab": "موارد", + "header.loading": "جار التحميل...", + "header.filterButton.simple": "مرشحات", + "header.filterButton.withCount": "({count}) مرشح", + "header.seeResults": "انظر النتائج", + "header.backButton": "عُد", + "header.contentSettingsButton.simple": "Menu", + "header.contentSettingsButton.withCount": "(مينو) {count} filter applied.Menu. {count}", + "navigation.about": "حول", + "navigation.licenses": "التراخيص", + "navigation.getInvolved": "شارك", + "navigation.api": "API", + "navigation.terms": "شروط", + "navigation.privacy": "خصوصية", + "navigation.feedback": "استجابة", + "navigation.sources": "مصادر", + "navigation.externalSources": "مصادر خارجية", + "navigation.searchHelp": "بحث في المساعدة", + "about.title": "حول {openverse}", + "about.description.content": "{openverse} هي أداة تسمح باكتشاف أعمال المجال العام والمرخصة بشكل مفتوح واستخدامها من قبل الجميع.", + "about.collection.content.a": "{openverse} searches across more than 800 million images and audio from open APIs and the {commonCrawl} dataset.", + "about.collection.content.b": "نحن نجمع العمل من عدة مستودعات عامة، ونسهل إعادة الاستخدام من خلال سمات مثل العزو البقعة الواحدة.", + "about.planning.content.a": "وحالياً، لا تقوم شركة {openverse} إلا بتفتيش الصور والصوت، والبحث عن الفيديو المقدم من مصادر خارجية.", + "about.planning.content.b": "ونخطط لإضافة أنواع إضافية من وسائط الإعلام مثل النصوص المفتوحة ونماذج الـ 3 دال، بهدف نهائي هو إتاحة إمكانية الوصول إلى ما يقدر بـ 2.5 بليون من دولارات الولايات المتحدة المرخص لها والمجالات العامة على الشبكة.", + "about.planning.content.c": "جميع رموزنا هي مصدر مفتوح ويمكن الوصول إليها في {working} نحن {repository} يمكنك أن ترى ما هو {community}", + "about.planning.repository": "مستودع {openverse} {github}", + "about.planning.community": "نرحب بمساهمة المجتمع", + "about.planning.working": "نحن نعمل حاليا على", + "about.transfer.content.a": "{openverse} is the successor to CC search which was launched by Creative Commons in 2019, after its migration to WordPress in 2021.", + "about.transfer.content.b": "يمكنك قراءة المزيد عن هذا الانتقال في الإعلانات الرسمية من {creativeCommons} and {wordpress}", + "about.transfer.content.c": "ولا نزال ملتزمين بتحقيق هدفنا المتمثل في معالجة إمكانية اكتشاف وسائل الإعلام المفتوحة والوصول إليها.", + "about.declaration.content.a": "{openverse} does not verify licensing information for individual works, or whether the generated attribution is accurate or complete.", + "about.declaration.content.b": "يرجى التحقق بشكل مستقل من حالة الترخيص والمعلومات المتعلقة بالإسناد قبل إعادة استخدام المحتوى. للحصول على المزيد من التفاصيل، قراءة {terms}", + "about.declaration.terms": "{openverse} شروط الاستخدام", + "sources.title": "مصادر", + "sources.detail": "يتيح لك النقر فوق {singleName} تصفح العناصر الموجودة داخل هذا المصدر وتصفيتها.", + "sources.singleName": "مصدر", + "sources.providers.source": "مصدر", + "sources.providers.domain": "اِختِصاص", + "sources.providers.item": "مجموع العناصر", + "sources.ccContent.where": "من أين يأتي المحتوى على {openverse}؟", + "sources.ccContent.content": "هناك محتوى مرخص بشكل علني مستضاف على ملايين المجالات عبر اتساع الإنترنت. يحدد فريقنا بشكل منهجي مقدمي الخدمة الذين يستضيفون محتوى مرخصًا بـ CC. إذا كان ملائمًا ، فنحن نفهرس هذا المحتوى ونجعله قابلاً للاكتشاف من خلال {openverse}.", + "sources.ccContent.provider.a": "ولدى بعض مقدمي الخدمات تجمعات متعددة مختلفة من المحتوى داخلهم. {flickr} has sources ranging from NASA to personal photography. The {smithsonian} comprises a dozen diverse collections.", + "sources.ccContent.provider.b": "ويدير مشاعات الويكيو القفازات من حيث المحتوى، ويستخدمها عدد من المجرات والمكتبات والمحفوظات والمتاحف التي تسلط الضوء على بعض أو كل مجموعاتها الرقمية.", + "sources.ccContent.europeana": "{openverse} ممتن بشكل خاص لعمل {link} ، وهي منظمة تعمل على رقمنة وإنشاء أعمال التراث الثقافي القابلة للاكتشاف في جميع أنحاء أوروبا. {openverse} قادر على فهرسة مئات المصادر القيمة من خلال تكامل واحد مع {linkApi}.", + "sources.newContent.next": "كيف نقرر ما هي المصادر التي يجب إضافتها بعد ذلك؟", + "sources.newContent.integrate": "لدينا قائمة لا تنتهي أبدًا بالمصادر المحتملة للبحث قبل التكامل. نسأل أنفسنا أسئلة مثل:", + "sources.newContent.impact": " ما هو تأثير أو أهمية هذا المصدر لمستخدمينا؟ إذا كان موجودًا داخل مزود مثل ويكيميديا كومنز ، فهل من المفيد لمستخدمينا أن يكونوا قادرين على التصفية حسب هذا المصدر مباشرة؟", + "sources.newContent.reuse": "هل معلومات الترخيص والإسناد معروضة بوضوح لتمكين إعادة الاستخدام الواثق؟", + "sources.newContent.totalItems": "كم عدد العناصر الإجمالية الجديدة أو الأنواع الجديدة من العناصر التي يمكننا تقديمها لمستخدمينا من خلال هذا التكامل؟ بعض المصادر عبارة عن تكاملات مباشرة ، بينما قد يكون البعض الآخر مصدرًا داخل المصدر.", + "sources.suggestions": "نحن نقدر الاقتراحات لمصادر جديدة من مجتمع المستخدمين لدينا.", + "sources.issueButton": "اقترح مصدر جديد", + "sources.aria.table": "جدول المصادر", + "sources.heading.image": "مصادر الصور", + "sources.heading.audio": "مصادر الصوت", + "externalSourcesPage.title": "بحث ميتا", + "externalSourcesPage.intro": "تم إنشاء {openverse} فوق كتالوج يقوم بفهرسة المحتوى المرخص لـ CC والمحتوى العام من مصادر محددة. تعرف على المزيد حول {link} الخاص بنا.", + "externalSourcesPage.link": "مصادر", + "externalSourcesPage.license.a": "ومع ذلك، هناك مصادر عديدة لوسائط الإعلام المرخص بها من قبل لجنة مكافحة الإرهاب ووسائط الإعلام العامة التي لم نتمكن بعد من إدراجها في بحث {openverse}.", + "externalSourcesPage.license.b": "This might be because they do not offer a public API, or that our contributors have not yet had time to integrate them into {openverse}", + "externalSourcesPage.license.c": "هذه مصادر قيّمة و نريد أن نتأكّد من أنّك قادر على إيجاد أفضل المواد المرخصة علناً، بغض النظر عن مكان وجودها.", + "externalSourcesPage.new.title": "هل يمكنني اقتراح مصادر خارجية جديدة؟", + "externalSourcesPage.new.content": "نعم من فضلك! أنشئ {issue} في مستودع GitHub أو أرسل إلينا {email} وأخبرنا بالمصادر الجديدة التي ترغب في تضمينها.", + "externalSourcesPage.new.issue": "القضية", + "externalSourcesPage.new.email": "البريد الإلكتروني", + "externalSourcesPage.why.title": "لماذا بنيت هذا؟", + "externalSourcesPage.why.content": "لسنوات عديدة ، قدمت Creative Commons لمستخدميها بوابة بحث مخصصة لمنصات البحث التي تحتوي على عوامل تصفية ترخيص CC مضمنة. في الواقع ، لا يزال هذا قيد الصيانة في {old}.", + "externalSourcesPage.why.new.a": "بالنسبة لمستخدمي موقع البحث عن الميثان، سيبدو سمة " المصادر الخارجية " في {openverse} مألوفة.", + "externalSourcesPage.why.new.b": "وكان الهدف هو ضمان عدم فقدان القدرة الوظيفية، ولكن يجري تحديثها وإدراجها في محرك البحث الجديد الخاص بمحتواها المرخص بشكل مفتوح.", + "externalSourcesPage.why.new.c": "وبالإضافة إلى ذلك، تستند سمة " المصادر الخارجية " إلى هذه الوظيفة، مما يتيح لنا أن نضيف بسرعة مصادر خارجية جديدة عند اكتشافها، وأن ندعم أنواع المحتوى الجديدة في المستقبل.", + "externalSourcesPage.why.ariaLabel": "استجابة", + "externalSourcesPage.why.feedbackSuggestions": "نأمل أن تستمتع ، وإذا كانت لديك اقتراحات للتحسين ، فاترك لنا {feedback}.", + "externalSourcesPage.why.feedbackLink": "استجابة", + "externalSourcesPage.relationships.a": "وهذه الوظيفة تتيح لنا أيضا البدء في المحادثات وبناء علاقات مع المصادر التي قد ترغب في إدراجها في قضية {openverse} في المستقبل.", + "externalSourcesPage.relationships.b": "وأخيرا، يمكننا أيضا أن نعرض مصادر خارجية لأنواع وسائط الإعلام التي لا ندرجها في {openverse}، ولكن نخطط لذلك.", + "externalSourcesPage.explanation": "يمكنك العثور على روابط لمصادر خارجية أسفل كل صفحة نتائج بحث {openverse} ؛ في صفحات عمليات البحث التي لا تظهر نتائج ؛ وعلى صفحات أنواع الوسائط التي لا ندعمها بعد ولكننا نعتزم ذلك.", + "privacy.title": "خصوصية", + "privacy.intro.content": "يسعى مشروع {openverse} إلى جعل خصوصية مستخدمينا وأمانهم أولوية. {openverse} تلتزم بـ {link}. يرجى الاطلاع على هذا المستند للحصول على وصف كامل لكيفية استخدام {openverse} وحمايته لأي معلومات تزودنا بها.", + "privacy.intro.link": "سياسة الخصوصية لجميع مواقع WordPress.org", + "privacy.cookies.title": "ملفات تعريف الارتباط", + "privacy.cookies.content.a": "{openverse} تستخدم البسكويت لتخزين المعلومات عن أفضليات الزائرين ومعلومات عن مروجهم على الشبكة. ونحن نستخدم هذه المعلومات لتحسين خبرة مستخدمي الموقع.", + "privacy.cookies.content.b": "هذه تعتبر \"الوكالة\" أو \"الكوكيز الضروري جداً\" قد تبطلين هذه من خلال تغيير أطرك للطوارئ، ولكن هذا قد يؤثر على كيفية وظائف {openverse}", + "privacy.contact.title": "اتصل بنا", + "privacy.contact.content": "يمكن إرسال أي أسئلة حول {openverse} والخصوصية إلى {email} ، أو مشاركتها على أنها {issue} ، أو مناقشتها مع مجتمعنا في #openverse قناة {chat}.", + "privacy.contact.issue": "مشكلة GitHub", + "privacy.contact.chat": "جعل WordPress دردشة", + "searchGuide.title": "{openverse} دليل البنية", + "searchGuide.intro": "عند البحث ، يمكنك إدخال رموز أو كلمات خاصة لمصطلح البحث الخاص بك لجعل نتائج البحث أكثر دقة.", + "searchGuide.exact.title": "ابحث عن تطابق تام", + "searchGuide.exact.ariaLabel": "اقتبس اقتباس كلود مونيه", + "searchGuide.exact.claudeMonet": "\"كلود مونيه\"", + "searchGuide.exact.content": "ضع كلمة أو عبارة داخل علامات الاقتباس. على سبيل المثال ، {link}.", + "searchGuide.negate.title": "المصطلحات المستبعدة", + "searchGuide.negate.operatorName": "ناقصا", + "searchGuide.negate.ariaLabel": "الكلب مطروحا منه", + "searchGuide.negate.example": "كلب", + "searchGuide.negate.content": "لاستبعاد مصطلح من نتائجك، وضع {operator} أمامه. Example: {link}{br} هذا سيبحث عن وسائل الإعلام ذات الصلة بـ\"الكلب\" لكن لن يتضمن النتائج ذات الصلة بـ \"بوغ\"", + "feedback.title": "استجابة", + "feedback.intro": "شكرًا لك على استخدام {openverse}! نرحب بأفكارك لتحسين الأداة أدناه. لتقديم ملاحظات منتظمة ، انضم إلى قناة {slack} في مساحة عمل {makingWordpress} Slack.", + "feedback.improve": "ساعدنا لنتحسن", + "feedback.report": "الإبلاغ عن خطأ", + "feedback.loading": "جار التحميل...", + "feedback.aria.improve": "ساعدنا في تحسين الشكل", + "feedback.aria.report": "الإبلاغ عن نموذج خطأ", + "sensitive.title": "المحتوى الحساس", + "sensitive.description.content.a": "{openverse} operates along a “safe-default” approach in all aspects of its operation and development, with the intention of being as inclusive and accessible as possible.", + "sensitive.description.content.b": "ومن ثم، فإن {openverse} لا تشمل سوى النتائج ذات المحتوى الحساس عندما اختار المستعملون صراحة " تحقيق نتائج حساسة " بشأن {openverseOrg} وفي {openverse} API.", + "sensitive.description.content.c": "In adherence to {wpCoc} and its {deiStatement}, {openverse} holds contributors to high expectations regarding conduct towards other contributors, the accessibility of contribution and the services, and, therefore, being an inclusive project.", + "sensitive.description.content.d": "Similarly, {openverse} holds the expectation that the results returned from the API or displayed on the {openverseOrg} should be accessible by default.", + "sensitive.description.content.e": "على الجميع، بغض النظر عن خلفيتهم، أن يشعروا بالأمان، وأنهم مشمولون في {openverse}، سواء كانوا مساهمين في الجوانب التقنية لخدمات {openverse}، وهو مبدع مشمول بأعماله في {openverse}", + "sensitive.description.content.f": "{openverse} recognises its responsibility as a tool used by people of a wide variety of ages, including young people in educational settings, and pays particular attention to minimizing accidental interaction with or exposure to sensitive content.", + "sensitive.description.wpCoc": "مدونة قواعد السلوك المجتمعية", + "sensitive.description.deiStatement": "التنوع والإنصاف وبيان الإدماج", + "sensitive.sensitivity.what.a": "{openverse} تستخدم مصطلح \"حساس\" بدلاً من \"المكانة\" أو \"الخدمة العامة\" (غير آمنة للعمل) أو مصطلحات أخرى للإشارة إلى أن تعريفنا للمحتوى حساس هو واسع، مع التركيز على إمكانية الوصول والإدماج.", + "sensitive.sensitivity.what.b": "وهذا يعني أن بعض المحتويات تُسمّى بـ \"حساسية\" التي لا تندرج في فئة مما يُفهم عموماً على أنها محتوى \"المكانة\" (أي بعبارة أخرى، المحتوى تحديداً لجمهور بالغ).", + "sensitive.sensitivity.what.c": "غير أن التسمية لا تعني ضمناً أن {openverse} أو متعهديها يعتبرون المضمون غير ملائم للمنبر بشكل عام، وهو أيضاً لا ينطوي على حكم أخلاقي أو أخلاقي.", + "sensitive.sensitivity.what.d": "نحن نعتبر المحتوى \"الحساس\" مُحتوى مُهين أو مُزعج، أو غير لائق، مع إيلاء اهتمام خاص للشباب.", + "sensitive.sensitivity.how.a": "ويتسم تعريف الحساسية هذا بدرجة كبيرة من المرونة وهو غير دقيق عن قصد.", + "sensitive.sensitivity.how.b": "وتعتمد شركة {openverse} على مجموعة متنوعة من الأدوات لاكتشاف المحتوى الذي يمكن أن يكون حساسا، بما في ذلك تقارير معتدلة للمستعملين عن العمل الفردي ومسح المحتوى النصي المتصل بالعمل ذي الحساسية.", + "sensitive.sensitivity.how.c": "ويرد أدناه وصف أكثر تفصيلا لهذه المسائل.", + "sensitive.onOff.title": "تغيير المحتوى الحساس", + "sensitive.onOff.sensitiveResults": "بالخطأ {openverse} does not include sensitive content in search results. ويتطلب إدراج النتائج الحساسة اختيارا صريحا من المستخدم. ويمكن للمستعمل أن يختار إدراج المحتوى الحساس في نتائج البحث بتمكين مفتاح " النتائج الحسية " .", + "sensitive.onOff.blurSensitive.a": "وعندما يُدرج المحتوى الحساس، تكون النتائج الحساسة التي تُعاد غير واضحة أيضاً لمنع التعرض العرضي.", + "sensitive.onOff.blurSensitive.b": "كما أن عدم طمسها يتطلب اختيارا صريحا من المستخدم. ويمكن للمستعمل أن يختار أن يرى محتوى حساسا غير واضح من خلال تبديل مفتاح " محتوى اللغم " .", + "sensitive.onOff.where": "وكلاهما متاحان في قاذفة التصفية (على الحواسيب المكتبية) وفي شريط " فليتر " لأماكن البحث (على الأجهزة المحمولة) على صفحة نتائج البحث.", + "sensitive.designations.title": "تعيينات المحتوى الحساسة", + "sensitive.designations.description.a": "{openverse} يعيّن محتوىً حساساً في API وفي موقع {openverseOrg} على شبكة الإنترنت باستخدام طريقتين: تقارير من مستعملي {openverse} واكتشاف النصوص الحساسة آلياً.", + "sensitive.designations.description.b": "ولا تقتصر هذه التسميات على بعضها البعض، ويمكن أن يطبق عليها عمل واحد أو كليهما.", + "sensitive.designations.userReported.title": "المستعمل أبلغ عن حساسية", + "sensitive.designations.userReported.description.a": "{openverse} users are invited to report sensitive content via the {openverseOrg} website and the {openverse} API.", + "sensitive.designations.userReported.description.b": "Some tools and apps that integrate with the {openverse} API, like the {gutenbergMediaInserter}, also allow their users to report sensitive content.", + "sensitive.designations.userReported.description.c": "وتشمل صفحة العمل الفردي القدرة على الإبلاغ عن المحتوى على أنه حساس (أو الإبلاغ عن انتهاكات الحقوق).", + "sensitive.designations.userReported.description.d": "وتحقق من هذه التقارير ويتخذون قرارات بشأن ما إذا كان ينبغي إضافة تسمية حساسية للعمل، أو في بعض الحالات، على النحو المبين أعلاه، شطب العمل من مكتب المدعي العام.", + "sensitive.designations.userReported.gutenbergMediaInserter": "Gutenberg editor’s {openverse}", + "sensitive.designations.sensitiveText.title": "المحتوى النصي الحساس", + "sensitive.designations.sensitiveText.description.a": "{openverse} يمسح بعض البيانات الوصفية النصية المتصلة بالأعمال التي توفرها مصادرنا بشروط حساسة.", + "sensitive.designations.sensitiveText.description.b": "{openverse} {sensitiveTermsList} is open source and contributions and input from the community are welcome and invited.", + "sensitive.designations.sensitiveText.description.c": "ومن أمثلة النصوص التي يمكن أن تكون حساسة النص، على سبيل المثال لا الحصر، نصوص ذات طابع جنسي أو بيولوجي أو عنيف أو عنصري أو غير ذلك من أشكال عدم التقيد.", + "sensitive.designations.sensitiveText.description.d": "The project recognises that this approach is imperfect and that some works may inadvertently receive a sensitivity nomination without necessarily being sensitive.", + "sensitive.designations.sensitiveText.description.e": "ولمزيد من السياق بشأن السبب الذي جعلنا نختار هذا النهج على الرغم من ذلك، نشير إلى {imperfect} من وثيقة تخطيط مشاريعنا المتصلة بهذه السمة.", + "sensitive.designations.sensitiveText.sensitiveTermsList": "قائمة المصطلحات الحساسة", + "sensitive.designations.sensitiveText.imperfect": "قسم شرطة لوس أنجلوس", + "sensitive.designations.sensitiveText.metadata.a": "It is important to note that some textual metadata for a work is {notAvailable} من خلال موقع {openverse} API أو موقع {openverseOrg}", + "sensitive.designations.sensitiveText.metadata.b": "غير أن هذه البيانات الوصفية لا تزال مقصودة بشروط حساسة ولا تعامل كحالة خاصة.", + "sensitive.designations.sensitiveText.metadata.c": "وفي حالة ما إذا وجد نص {openverse} شروطا حساسة في حقول البيانات الفوقية هذه للعمل، فإن العمل سيظل يتلقى تعيينا حساسا يستند إلى نص حساس حتى وإن لم يكن النص الحساس نفسه متاحا عن طريق {openverse}", + "sensitive.designations.sensitiveText.metadata.d": "وتأخذ شركة {openverse} النهج القائل بأن المحتوى النصي الحساس في وصف ما هو مؤشر عال نسبيا من مؤشرات الأعمال الحساسة المحتملة.", + "sensitive.designations.sensitiveText.metadata.e": "As above, {openverse} understands that this is not perfect.", + "sensitive.designations.sensitiveText.notAvailable": "غير متاحة", + "sensitive.faq.title": "الأسئلة المتكررة", + "sensitive.faq.one.question": "وقد وجدت المحتوى الذي أعتقد أنه حساس لا يوجد فيه تحديد حساسية. ماذا يجب أن أفعل؟", + "sensitive.faq.one.answer.a": "يرجى الإبلاغ عن المحتوى الحساس عن طريق زيارة صفحة العمل الفردي على موقع {openverseOrg} على شبكة الإنترنت، واستخدام زر " إعادة الإبلاغ عن هذا المحتوى " تحت معلومات الإسناد وفوق البطاقات.", + "sensitive.faq.one.answer.b": "{openverse}", + "sensitive.faq.two.question": "لا أوافق على تحديد الحساسية في العمل هل يمكنك أن تزيله من فضلك؟", + "sensitive.faq.two.answerPt1": "بالنسبة للتسميات القائمة على النصوص، {openverse} does not at the moment have a method for removing the nomination. This is a feature that will be built eventually, but is not part of the baseline sensitive content detection feature.", + "sensitive.faq.two.answerPt2.a": "وفيما يتعلق بالتسميات التي أبلغ عنها المستعمل، يرجى تقديم تقرير جديد عن صفحة العمل بعد التعليمات الواردة في السؤال السابق.", + "sensitive.faq.two.answerPt2.b": "في الملاحظات، وصف لماذا تعتقد أن العمل لا ينبغي أن يكون له تحديد حساسية.", + "sensitive.faq.two.answerPt2.c": "As when added a new nomination, {openverse} reserves the right to respectfully decline the request to remove a confirmed user sensitivity nomination.", + "sensitive.faq.three.question": "وقد وجدت محتوى في {openverse} قد يكون غير قانوني. إلى جانب إبلاغه إلى {openverse}، هل هناك أي خطوات أخرى يمكنني اتخاذها؟", + "sensitive.faq.three.answer.a": "وفيما يتعلق بالتسميات التي أبلغ عنها المستعمل، يرجى تقديم تقرير جديد عن صفحة العمل بعد التعليمات الواردة في السؤال السابق.", + "sensitive.faq.three.answer.b": "في الملاحظات، وصف لماذا تعتقد أن العمل لا ينبغي أن يكون له تحديد حساسية.", + "sensitive.faq.three.answer.c": "As when added a new nomination, {openverse} reserves the right to respectfully decline the request to remove a confirmed user sensitivity nomination.", + "tags.title": "Understanding Tags in {openverse}", + "tags.intro.a": "وقد يكون لكل عمل إبداعي في {openverse} بطاقات، ومجموعة اختيارية من الكلمات الرئيسية المستخدمة لوصف العمل وجعل من الأسهل للمستعملين أن يجدوا وسائل الإعلام ذات الصلة للبحث عنهم.", + "tags.intro.b": "وتنقسم هذه البطاقات إلى فئتين رئيسيتين: علامات المصدر والعلامات المولدة. فهم الفرق بينهما يمكن أن يعزز خبرتك في البحث ويحسن دقة نتائجك", + "tags.sourceTags.title": "المصدر", + "tags.sourceTags.content.a": "وعلامات المصدر هي علامات نشأت من المصدر الأصلي للعمل الإبداعي. ويمكن أن يضاف المساهمون المختلفون هذه البطاقات، مثل المصور الذي قام بتحميل صورتهم إلى فليكر والعلامات الوصفية الإضافية.", + "tags.sourceTags.content.b": "ويمكن للمنبر الأصلي نفسه أن يخصص بطاقات إضافية من أعضاء المجتمع المحلي أو التشغيل الآلي أو مصادر أخرى.", + "tags.generatedTags.title": "تاغس المولدة", + "tags.generatedTags.content.a": "وتُنشأ العلامات المولدة من خلال التحليل الآلي للأشغال الإبداعية، ومعظم الصور الشائعة. وتنطوي هذه العملية على تكنولوجيات متطورة مثل AWS Rekognition ، وكلاريفاي، وغير ذلك من خدمات التعرف على الصور التي تحلل المحتوى وتولد علامات وصفية.", + "tags.generatedTags.content.b": "وفي حين أن النظم الآلية الموثوق بها عموما يمكن أن تسيئ أحيانا تفسير أو تفوت عناصر في صورة ما.", + "tags.generatedTags.content.c": "وتبذل الجهود لاستبعاد أي علامات متولدة تشير إلى هويات أو انتماءات البشر.", + "tags.generatedTags.content.d": "إذا واجهتم أي صور بعلامات متولدة تضعون افتراضات حول، على سبيل المثال، الجنس أو الدين أو الانتماء السياسي، يرجى الإبلاغ عن الصور باستخدام زر \" التقرير \" على صفحات النتائج الوحيدة.", + "error.occurred": "حدث خطأ", + "error.imageNotFound": "تعذر العثور على الصورة بالمعرف {id}", + "error.mediaNotFound": "تعذر العثور على {mediaType} بالمعرف {id}", + "error.image": "صورة", + "error.audio": "صوتي", + "filters.title": "مرشحات", + "filters.filterBy": "مصنف بواسطة", + "filters.licenses.title": "التراخيص", + "filters.licenses.cc0": "CC0", + "filters.licenses.pdm": "علامة المجال العام", + "filters.licenses.by": "بواسطة", + "filters.licenses.bySa": "BY-SA", + "filters.licenses.byNc": "BY-NC", + "filters.licenses.byNd": "BY-ND", + "filters.licenses.byNcSa": "BY-NC-SA", + "filters.licenses.byNcNd": "BY-NC-ND", + "filters.licenseTypes.title": "يستخدم", + "filters.licenseTypes.commercial": "استخدام تجاريًا", + "filters.licenseTypes.modification": "تعديل أو تكييف", + "filters.imageProviders.title": "مصدر", + "filters.audioProviders.title": "مصدر", + "filters.audioCategories.title": "فئة الصوت", + "filters.audioCategories.audiobook": "كتاب مسموع", + "filters.audioCategories.music": "موسيقى", + "filters.audioCategories.news": "أخبار", + "filters.audioCategories.podcast": "تدوين صوتي", + "filters.audioCategories.pronunciation": "النطق", + "filters.audioCategories.sound_effect": "مؤثرات صوتية", + "filters.audioCategories.sound": "مؤثرات صوتية", + "filters.imageCategories.title": "نوع الصورة", + "filters.imageCategories.photograph": "الصور", + "filters.imageCategories.illustration": "الرسوم التوضيحية", + "filters.imageCategories.digitized_artwork": "الأعمال الفنية الرقمية", + "filters.audioExtensions.title": "امتداد", + "filters.audioExtensions.flac": "FLAC", + "filters.audioExtensions.mid": "منتصف", + "filters.audioExtensions.mp3": "MP3", + "filters.audioExtensions.oga": "OGA", + "filters.audioExtensions.ogg": "OGG", + "filters.audioExtensions.opus": "OPUS", + "filters.audioExtensions.wav": "WAV", + "filters.audioExtensions.webm": "WEBM", + "filters.imageExtensions.title": "امتداد", + "filters.imageExtensions.jpg": "JPEG", + "filters.imageExtensions.png": "بي إن جي", + "filters.imageExtensions.gif": "GIF", + "filters.imageExtensions.svg": "SVG", + "filters.aspectRatios.title": "ابعاد متزنة", + "filters.aspectRatios.tall": "طويل", + "filters.aspectRatios.wide": "واسع", + "filters.aspectRatios.square": "ميدان", + "filters.sizes.title": "حجم الصورة", + "filters.sizes.small": "صغير", + "filters.sizes.medium": "متوسط", + "filters.sizes.large": "كبير", + "filters.safeBrowsing.title": "الحشد الآمن", + "filters.safeBrowsing.desc": "ولا يظهر التخلف المضمون الذي يُعرف باسم {sensitive}..", + "filters.safeBrowsing.sensitive": "حساسة", + "filters.safeBrowsing.toggles.fetchSensitive.title": "النتائج الحساسة", + "filters.safeBrowsing.toggles.fetchSensitive.desc": "واتسمت النتائج بالحساسية في مجال النتائج.", + "filters.safeBrowsing.toggles.blurSensitive.title": "محتوى البق", + "filters.safeBrowsing.toggles.blurSensitive.desc": "الصور والنصوص لمنع رؤية المواد الحساسة", + "filters.lengths.title": "مدة", + "filters.lengths.shortest": "<30 ثانية", + "filters.lengths.short": "30 ثانية -2 دقيقة", + "filters.lengths.medium": "2-10 دقائق", + "filters.lengths.long": "> 10 دقائق", + "filters.creator.title": "البحث حسب المنشئ", + "filters.searchBy.title": "البحث عن طريق", + "filters.searchBy.creator": "المنشئ", + "filters.licenseExplanation.licenseDefinition": "تعريف الترخيص", + "filters.licenseExplanation.markDefinition": "تعريف {mark}", + "filters.licenseExplanation.more.license": "{readMore} حول هذا الترخيص.", + "filters.licenseExplanation.more.mark": "{readMore} حول {mark}.", + "filters.licenseExplanation.more.readMore": "اقرأ أكثر", + "filters.aria.removeFilter": "إزالة مرشح {label}", + "filterList.filterBy": "مصنف بواسطة", + "filterList.hide": "إخفاء عوامل التصفية", + "filterList.clear": "مسح عوامل التصفية", + "filterList.clearNumbered": "مرشحات واضحة ({number})", + "filterList.show": "أظهر النتائج", + "filterList.categoryAria": "قائمة عوامل التصفية لفئة {categoryName}", + "browsePage.allNoResults": "لا نتائج", + "browsePage.allResultCount": "{localeCount} نتيجة | {localeCount} نتيجة", + "browsePage.allResultCountMore": "أكثر من {localeCount} نتيجة | أكثر من {localeCount} نتيجة", + "browsePage.contentLink.image.zero": "لا توجد صور لـ {query}", + "browsePage.contentLink.image.count": "See {localeCount} image found for {query}.|See {localeCount} images found for {query}.", + "browsePage.contentLink.image.countMore": "انظر على صور {query} {localeCount}", + "browsePage.contentLink.audio.zero": "لم يعثر على أي صوت لـ {query}.", + "browsePage.contentLink.audio.count": "انظر {localeCount} {query}", + "browsePage.contentLink.audio.countMore": "انظر فوق {query} {localeCount}", + "browsePage.load": "تحميل المزيد من النتائج", + "browsePage.loading": "جار التحميل...", + "browsePage.fetchingError": "خطأ في جلب {type}:", + "browsePage.searchRating.content": "هل هذه النتائج ذات صلة؟", + "browsePage.searchRating.yes": "نعم", + "browsePage.searchRating.no": "رقم", + "browsePage.searchRating.feedbackThanks": "شكرا لملاحظاتك!", + "browsePage.searchForm.placeholder": "بحث في كل {type}", + "browsePage.searchForm.image": "الصور", + "browsePage.searchForm.audio": "صوتي", + "browsePage.searchForm.video": "أشرطة فيديو", + "browsePage.searchForm.model3d": "نماذج ثلاثية الأبعاد", + "browsePage.searchForm.all": "المحتوى", + "browsePage.searchForm.collectionPlaceholder": "بحث في هذه المجموعة", + "browsePage.searchForm.button": "يبحث", + "browsePage.searchForm.clear": "آمن", + "browsePage.licenseDescription.title": "رخصة CC", + "browsePage.licenseDescription.by": "ائتمن الخالق.", + "browsePage.licenseDescription.nc": "الاستخدامات غير التجارية فقط.", + "browsePage.licenseDescription.nd": "لا مشتقات أو تعديلات مسموح بها.", + "browsePage.licenseDescription.sa": "شارك التعديلات تحت نفس الشروط.", + "browsePage.licenseDescription.zero": "تم وضع علامة على هذا العمل على أنه مخصص للملك العام.", + "browsePage.licenseDescription.pd": "تم وضع علامة على هذا العمل على أنه في المجال العام.", + "browsePage.licenseDescription.samplingPlus": "العينات والمزج والتحولات الإبداعية مسموح بها.", + "browsePage.aria.close": "أغلق", + "browsePage.aria.scroll": "انتقل إلى أعلى", + "browsePage.aria.search": "بحث", + "browsePage.aria.removeFilter": "إزالة عامل التصفية", + "browsePage.aria.licenseExplanation": "شرح الترخيص", + "browsePage.aria.creator": "البحث عن طريق المنشئ", + "browsePage.aria.imageTitle": "Image: {title}", + "browsePage.aria.audioTitle": "Audio: {title}", + "browsePage.aria.resultsLabel.all": "جميع النتائج لـ {query}", + "browsePage.aria.resultsLabel.image": "Image results for {query}", + "browsePage.aria.resultsLabel.audio": "Audio tracks for {query}", + "browsePage.aria.results.all": "جميع النتائج لـ \"{query}\" و{imageResults} و{audioResults}.", + "browsePage.aria.results.image.zero": "لا توجد نتائج للصور لـ \"{query}\"", + "browsePage.aria.results.image.count": "{localeCount} {query}", + "browsePage.aria.results.image.countMore": "أهم نتائج الصور {localeCount} لـ \"{query}\".", + "browsePage.aria.results.audio.zero": "لا توجد آثار صوتية لـ \"{query}\"", + "browsePage.aria.results.audio.count": "{localeCount} {query}", + "browsePage.aria.results.audio.countMore": "أهم المقاطع الصوتية {localeCount} لـ \"{query}\".", + "browsePage.aria.allResultsHeadingCount.image.zero": "لا صور", + "browsePage.aria.allResultsHeadingCount.image.count": "{localeCount} image.{localeCount} images", + "browsePage.aria.allResultsHeadingCount.image.countMore": "أعلى صور {localeCount}", + "browsePage.aria.allResultsHeadingCount.audio.zero": "لا آثار صوتية", + "browsePage.aria.allResultsHeadingCount.audio.count": "{localeCount} {localeCount}", + "browsePage.aria.allResultsHeadingCount.audio.countMore": "أعلى {localeCount}", + "mediaDetails.information.type": "يكتب", + "mediaDetails.information.unknown": "مجهول", + "mediaDetails.information.category": "فئة", + "mediaDetails.scroll.forward": "تقدم للأمام", + "mediaDetails.scroll.back": "تراجع", + "mediaDetails.reuse.title": "كيف تستعمل", + "mediaDetails.reuse.description": "قم بزيارة موقع ويب {media} لتنزيله واستخدامه. تأكد من أن تنسب الفضل إلى المنشئ من خلال إظهار معلومات الإسناد حيث تشارك عملك.", + "mediaDetails.reuse.copyrightDisclaimer": "قد تحتوي بعض الصور الفوتوغرافية على محتوى محمي بحقوق الطبع والنشر، مثل اللوحات أو المنحوتات أو الأعمال المعمارية. قد يتطلب استخدام هذه الصور الحصول على أذونات إضافية من صاحب حقوق الطبع والنشر للأعمال المصورة.", + "mediaDetails.reuse.licenseHeader": "رخصة", + "mediaDetails.reuse.toolHeader": "المجال العام", + "mediaDetails.reuse.audio": "صوتي", + "mediaDetails.reuse.image": "صورة", + "mediaDetails.reuse.tool.content": "اقرأ المزيد عن الأداة {link}.", + "mediaDetails.reuse.tool.link": "هنا", + "mediaDetails.reuse.credit.genericTitle": "هذا العمل", + "mediaDetails.reuse.credit.actualTitle": "\"{title}\"", + "mediaDetails.reuse.credit.text": "{title} {creator} {markedLicensed} {license}. {viewLegal}", + "mediaDetails.reuse.credit.creatorText": "بواسطة {creatorName}", + "mediaDetails.reuse.credit.marked": "تم وضع علامة", + "mediaDetails.reuse.credit.licensed": "مرخص بموجب", + "mediaDetails.reuse.credit.viewLegalText": "لعرض {termsCopy} ، قم بزيارة {url}.", + "mediaDetails.reuse.credit.termsText": "الشروط", + "mediaDetails.reuse.credit.copyText": "نسخة من هذا الترخيص", + "mediaDetails.reuse.copyLicense.title": "انسب الفضل إلى المنشئ", + "mediaDetails.reuse.copyLicense.rich": "النص الغني", + "mediaDetails.reuse.copyLicense.html": "لغة البرمجة", + "mediaDetails.reuse.copyLicense.plain": "نص عادي", + "mediaDetails.reuse.copyLicense.copyText": "نسخ النص", + "mediaDetails.reuse.copyLicense.copied": "نسخ!", + "mediaDetails.reuse.copyLicense.xml": "إك سم إل", + "mediaDetails.reuse.attribution": "تم تمييز هذه الصورة بترخيص {link}:", + "mediaDetails.providerLabel": "مزود", + "mediaDetails.sourceLabel": "مصدر", + "mediaDetails.providerDescription": "الموقع الشبكي الذي يستضيف فيه المحتوى", + "mediaDetails.sourceDescription": "المنظمة التي أنشأت أو تملك المحتوى الأصلي", + "mediaDetails.loading": "جار التحميل...", + "mediaDetails.relatedError": "خطأ في جلب الوسائط ذات الصلة", + "mediaDetails.aria.attribution.license": "اقرأ المزيد عن الترخيص", + "mediaDetails.aria.attribution.tool": "اقرأ المزيد عن الأداة", + "mediaDetails.aria.creatorUrl": "اسم {creator}", + "mediaDetails.imageInfo": "معلومات الصورة", + "mediaDetails.audioInfo": "معلومات صوتية", + "mediaDetails.tags.title": "العلامات", + "mediaDetails.tags.generated.heading": "العلامات المولدة", + "mediaDetails.tags.generated.pageTitle": "تعلم المزيد", + "mediaDetails.tags.source.heading": "بطاقات المصدر", + "mediaDetails.tags.showMore": "أظهر المزيد", + "mediaDetails.tags.showLess": "تظهر أقل", + "mediaDetails.contentReport.short": "تقرير", + "mediaDetails.contentReport.long": "الإبلاغ عن هذا المحتوى", + "mediaDetails.contentReport.form.disclaimer": "لأغراض أمنية ، يجمع {openverse} ويحتفظ بعناوين IP مجهولة المصدر لأولئك الذين يكملون هذا النموذج ويرسلونه.", + "mediaDetails.contentReport.form.question": "ماهو السبب؟", + "mediaDetails.contentReport.form.dmca.option": "ينتهك حقوق الطبع والنشر", + "mediaDetails.contentReport.form.dmca.note": "يجب عليك ملء هذا {form} للإبلاغ عن انتهاك حقوق النشر. لن يتم اتخاذ أي إجراء حتى يتم ملء هذا النموذج وإرساله. نوصي بعمل الشيء نفسه في المصدر ، {source}.", + "mediaDetails.contentReport.form.dmca.form": "نموذج DMCA", + "mediaDetails.contentReport.form.dmca.open": "فتح النموذج", + "mediaDetails.contentReport.form.sensitive.option": "يحتوي على محتوى حساس", + "mediaDetails.contentReport.form.sensitive.subLabel": "اختياري", + "mediaDetails.contentReport.form.sensitive.placeholder": "اختياريًا ، قدم وصفًا.", + "mediaDetails.contentReport.form.other.option": "آخر", + "mediaDetails.contentReport.form.other.note": "صف القضية.", + "mediaDetails.contentReport.form.other.subLabel": "مطلوب", + "mediaDetails.contentReport.form.other.placeholder": "الرجاء إدخال 20 حرفًا على الأقل.", + "mediaDetails.contentReport.form.submit": "تقرير", + "mediaDetails.contentReport.form.cancel": "يلغي", + "mediaDetails.contentReport.success.title": "تم إرسال التقرير بنجاح", + "mediaDetails.contentReport.success.note": "شكرا على الإبلاغ هذا المحتوى. نوصي بعمل الشيء نفسه في المصدر ، {source}.", + "mediaDetails.contentReport.failure.title": "تعذر تقديم التقرير", + "mediaDetails.contentReport.failure.note": "حدث خطأ ما ، يرجى المحاولة مرة أخرى بعد قليل.", + "singleResult.back": "رجوع إلى نتائج البحث", + "imageDetails.creator": "بالاسم{name}", + "imageDetails.weblink": "احصل على هذه الصورة", + "imageDetails.information.dimensions": "أبعاد", + "imageDetails.information.pixels": "بكسل", + "imageDetails.information.sizeInPixels": "{width} × {height} بكسل", + "imageDetails.relatedImages": "الصور ذات الصلة", + "imageDetails.aria.creatorUrl": "المؤلف {creator}", + "audioDetails.genreLabel": "النوع", + "audioDetails.relatedAudios": "الصوت ذو الصلة", + "audioDetails.table.album": "الألبوم", + "audioDetails.table.sampleRate": "معدل العينة", + "audioDetails.table.filetype": "شكل", + "audioDetails.table.genre": "النوع", + "audioDetails.weblink": "احصل على هذا الصوت", + "allResults.snackbar.text": "Press {spacebar} to play or pause the track.", + "allResults.snackbar.spacebar": "Spacebar", + "audioResults.snackbar.text": "Press {spacebar} to play or pause, and {left} "{right} to seek through the track.", + "audioResults.snackbar.spacebar": "Spacebar", + "audioResults.snackbar.left": "♪", + "audioResults.snackbar.right": "?", + "externalSources.caption": "{openverse} لا يتحكم في النتائج ، تحقق دائمًا من أن العمل في الواقع خاضع لترخيص CC.", + "externalSources.button": "قائمة مصدر", + "externalSources.title": "مصادر خارجية", + "externalSources.card.search": "لا تجد ما تبحث عنه؟ جرب المصادر الخارجية", + "externalSources.card.caption": "انقر فوق أحد المصادر أدناه للبحث مباشرة في مجموعات أخرى من الصور المرخصة CC. {break} يرجى ملاحظة أن استخدام المرشحات غير مدعوم لمكتبة Open Clip Art Library أو Nappy.", + "externalSources.form.supportedTitle": "لا تجد ما تبحث عنه؟ ابحث في مصادر خارجية", + "externalSources.form.supportedTitleSm": "ابحث في مصادر خارجية", + "browsers.chrome": "كروم", + "browsers.firefox": "ثعلب النار", + "browsers.opera": "Opera", + "browsers.edge": "حافة", + "waveform.label": "شريط البحث عن الصوت", + "waveform.currentTime": "{time} ثانية | {time} ثانية", + "audioThumbnail.alt": "صورة الغلاف لـ \"{title}\" بواسطة {creator}", + "audioTrack.ariaLabel": "{title} - مشغل الصوت", + "audioTrack.ariaLabelInteractive": "{title} - مشغل الصوت - اضغط على مفتاح المسافة لتشغيل معاينة الصوت وإيقافها مؤقتًا", + "audioTrack.ariaLabelInteractiveSeekable": "Audio: {title} - اللاعب التفاعلي - اضغط على حانة الفضاء لتلعب وتتوقف عن النظرة المسبقة للصوت؛ استخدام مفاتيح السهم اليسرى واليمن للبحث عن طريق المسار.", + "audioTrack.messages.err_aborted": "لقد أجهضت التشغيل.", + "audioTrack.messages.err_network": "حدث خطأ في الشبكة.", + "audioTrack.messages.err_decode": "تعذر فك تشفير الصوت.", + "audioTrack.messages.err_unallowed": "لا يسمح بالاستنساخ.", + "audioTrack.messages.err_unknown": "حدث خطأ غير متوقع حاول مرة أخرى في بضع دقائق أو الإبلاغ عن البند إذا استمرت المسألة.", + "audioTrack.messages.err_unsupported": "لا يدعم متصفحك تنسيق الصوت هذا.", + "audioTrack.messages.loading": "جار التحميل...", + "audioTrack.creator": "بواسطة {creator}", + "audioTrack.close": "أغلق مشغل الصوت", + "playPause.play": "لعب", + "playPause.pause": "يوقف", + "playPause.replay": "إعادة", + "playPause.loading": "جار التحميل", + "search.search": "يبحث", + "search.searchBarLabel": "البحث عن محتوى في {openverse}", + "licenseReadableNames.cc0": "صفر", + "licenseReadableNames.pdm": "علامة المجال العام", + "licenseReadableNames.by": "الإسناد", + "licenseReadableNames.bySa": "نَسب المُصنَّف - شارك - على حدٍ سواء", + "licenseReadableNames.byNc": "نَسب المُصنَّف - غير تجاري", + "licenseReadableNames.byNd": "Attribution-NoDerivatives", + "licenseReadableNames.byNcSa": "Attribution-NonCommercial-Share-Alike", + "licenseReadableNames.byNcNd": "نسب المصنف غير التجاري لا المشتقات", + "licenseReadableNames.sampling+": "سامبلينغ بلس", + "licenseReadableNames.ncSampling+": "أخذ العينات غير التجارية زائد", "interpunct": "•", - "modal": { - "close": "قريب", - "ariaClose": "أغلق النموذج", - "closeNamed": "إغلاق {name}", - "closeContentSettings": "اغلق قائمة المحتويات", - "closePagesMenu": "اغلق قائمة الصفحات", - "closeBanner": "اغلق اللافتة" - }, - "errorImages": { - "depressedMusician": "عازف البيانو المكتئب يضع رأسه في أيديهم.", - "waitingForABite": "يجلس ثلاثة أولاد على جذع شجرة مكسور بينما يصطاد اثنان منهم." - }, - "noResults": { - "heading": "لم نتمكن من العثور على أي شيء لـ \"{query}\".", - "alternatives": "جرب استعلامًا مختلفًا أو استخدم أحد المصادر الخارجية لتوسيع نطاق البحث." - }, - "serverTimeout": { - "heading": "عفوًا ، يبدو أن هذا الطلب استغرق وقتًا طويلاً لإكماله. حاول مرة اخرى." - }, - "unknownError": { - "heading": "عفوًا ، يبدو أنه حدث خطأ ما. حاول مرة اخرى." - }, - "searchType": { - "image": "الصور", - "audio": "صوتي", - "all": "كل المحتوى", - "video": "أشرطة فيديو", - "model3d": "نماذج ثلاثية الأبعاد", - "label": "نوع المحتوى للبحث", - "heading": "أنواع المحتويات", - "additional": "مصادر إضافية", - "statusBeta": "بيتا", - "seeImage": "مشاهدة جميع الصور", - "seeAudio": "مشاهدة كل الملفات الصوتية", - "selectLabel": "حدد نوع المحتوى: {type}" - }, + "modal.close": "قريب", + "modal.ariaClose": "أغلق النموذج", + "modal.closeNamed": "إغلاق {name}", + "modal.closeContentSettings": "اغلق قائمة المحتويات", + "modal.closePagesMenu": "اغلق قائمة الصفحات", + "modal.closeBanner": "اغلق اللافتة", + "errorImages.depressedMusician": "عازف البيانو المكتئب يضع رأسه في أيديهم.", + "errorImages.waitingForABite": "يجلس ثلاثة أولاد على جذع شجرة مكسور بينما يصطاد اثنان منهم.", + "noResults.heading": "لم نتمكن من العثور على أي شيء لـ \"{query}\".", + "noResults.alternatives": "جرب استعلامًا مختلفًا أو استخدم أحد المصادر الخارجية لتوسيع نطاق البحث.", + "serverTimeout.heading": "عفوًا ، يبدو أن هذا الطلب استغرق وقتًا طويلاً لإكماله. حاول مرة اخرى.", + "unknownError.heading": "عفوًا ، يبدو أنه حدث خطأ ما. حاول مرة اخرى.", + "searchType.image": "الصور", + "searchType.audio": "صوتي", + "searchType.all": "كل المحتوى", + "searchType.video": "أشرطة فيديو", + "searchType.model3d": "نماذج ثلاثية الأبعاد", + "searchType.label": "نوع المحتوى للبحث", + "searchType.heading": "أنواع المحتويات", + "searchType.additional": "مصادر إضافية", + "searchType.statusBeta": "بيتا", + "searchType.seeImage": "مشاهدة جميع الصور", + "searchType.seeAudio": "مشاهدة كل الملفات الصوتية", + "searchType.selectLabel": "حدد نوع المحتوى: {type}", "skipToContent": "تخطى الى المحتوى", - "prefPage": { - "title": "التفضيلات", - "groups": { - "analytics": { - "title": "تحليلات", - "desc": "يستخدم {openverse} تحليلات مجهولة لتحسين خدماتنا. نحن لا نجمع أي معلومات يمكن استخدامها لتحديد هويتك شخصيًا. ومع ذلك ، إذا كنت لا ترغب في المشاركة ، يمكنك إلغاء الاشتراك هنا." - } - }, - "features": { - "analytics": "سجل الأحداث المخصصة وطرق عرض الصفحة." - }, - "nonSwitchable": { - "title": "ميزات غير قابلة للتحويل", - "desc": "لا يمكنك تعديل حالة هذه الميزات." - }, - "switchable": { - "title": "ميزات قابلة للتبديل", - "desc": "يمكنك تشغيل هذه الميزات أو إيقاف تشغيلها كما تريد وسيتم حفظ تفضيلاتك في ملف تعريف ارتباط." - }, - "storeState": "حالة المتجر", - "contentFiltering": "تصفية المحتوى", - "explanation": "يظهر لأن {featName} هي {featState}" - }, + "prefPage.title": "التفضيلات", + "prefPage.groups.analytics.title": "تحليلات", + "prefPage.groups.analytics.desc": "يستخدم {openverse} تحليلات مجهولة لتحسين خدماتنا. نحن لا نجمع أي معلومات يمكن استخدامها لتحديد هويتك شخصيًا. ومع ذلك ، إذا كنت لا ترغب في المشاركة ، يمكنك إلغاء الاشتراك هنا.", + "prefPage.features.analytics": "سجل الأحداث المخصصة وطرق عرض الصفحة.", + "prefPage.nonSwitchable.title": "ميزات غير قابلة للتحويل", + "prefPage.nonSwitchable.desc": "لا يمكنك تعديل حالة هذه الميزات.", + "prefPage.switchable.title": "ميزات قابلة للتبديل", + "prefPage.switchable.desc": "يمكنك تشغيل هذه الميزات أو إيقاف تشغيلها كما تريد وسيتم حفظ تفضيلاتك في ملف تعريف ارتباط.", + "prefPage.storeState": "حالة المتجر", + "prefPage.contentFiltering": "تصفية المحتوى", + "prefPage.explanation": "يظهر لأن {featName} هي {featState}", "sketchfabIframeTitle": "عارض {sketchfab}", - "flagStatus": { - "nonexistent": "غير موجود", - "on": "على", - "off": "عن" - }, - "footer": { - "wordpressAffiliation": "جزء من مشروع {wordpress}", - "wip": "🚧" - }, - "language": { - "language": "لغة" - }, - "recentSearches": { - "heading": "عمليات البحث الأخيرة", - "clear": { - "text": "مسح", - "label": "مسح عمليات البحث الأخيرة" - }, - "clearSingle": { - "label": "تم البحث مؤخراً عن '{entry}'" - }, - "none": "لا توجد عمليات بحث حديثة لعرضها.", - "disclaimer": "لا يقوم Openverse بتخزين عمليات البحث الأخيرة ، يتم الاحتفاظ بهذه المعلومات محليًا في متصفحك." - }, - "report": { - "imageDetails": "انظر تفاصيل الصورة" - }, - "sensitiveContent": { - "title": { - "image": "هذه الصورة قد تحتوي على محتوى حساس", - "audio": "هذا المسار الصوتي قد يحتوي على محتوى حساس" - }, - "creator": "Creator", - "singleResult": { - "title": "المحتوى الحساس", - "hide": "إخفاء المحتوى", - "show": "محتوى العرض", - "explanation": "وهذا العمل يتسم بالحساسية للأسباب التالية:", - "learnMore": "{link} حول كيفية تعامل {openverse} مع المحتوى الحساس.", - "link": "تعلم المزيد" - }, - "reasons": { - "providerSuppliedSensitive": "وقد اعتبر مصدر هذا العمل أنه حساس.", - "sensitiveText": "{openverse} رصدت نصا يمكن أن يكون حساسا.", - "userReportedSensitive": "{openverse} Use have reported this work as sensitive." - } - }, - "collection": { - "heading": { - "tag": "الكلمات الدلالية", - "creator": "مبتدع", - "source": "منشأ" - }, - "pageTitle": { - "tag": { - "audio": "{tag}", - "image": "صور {tag}" - }, - "source": { - "audio": "{source}", - "image": "صور {source}" - } - }, - "link": { - "source": "موقع مفتوح المصدر", - "creator": "افتح صفحة المنشئ" - }, - "ariaLabel": { - "creator": { - "audio": "الملفات الصوتية بواسطة {creator}", - "image": "الصور عن طريق {creator}" - }, - "source": { - "audio": "ملفات صوتية من {source}", - "image": "الصور عن طريق {source}" - }, - "tag": { - "audio": "الملفات الصوتية مع العلامة {tag}", - "image": "الصور التي تحمل الوسم {tag}" - } - }, - "resultCountLabel": { - "creator": { - "audio": { - "zero": "لا توجد ملفات صوتية من قبل هذا الصانع", - "count": "{count}", - "countMore": "على الملفات الصوتية من قبل هذا الصانع" - }, - "image": { - "zero": "لا صور من هذا الصانع", - "count": "{count} صورة من هذا المبتكر.", - "countMore": "على صور {count} بواسطة هذا الصانع." - } - }, - "source": { - "audio": { - "zero": "No audio files provided by this source", - "count": "{count} audio file provided by this source. {count}", - "countMore": "Top {count} audio files provided by this source" - }, - "image": { - "zero": "No images provided by this source", - "count": "{count} image provided by this source. {count} images provided by this source", - "countMore": "Top {count} images provided by this source" - } - }, - "tag": { - "audio": { - "zero": "لا ملفات صوتية مع علامة مختارة", - "count": "{count} {count}", - "countMore": "Top {count} audio files with the selected tag" - }, - "image": { - "zero": "لا صور مع العلامة المختارة", - "count": "{count} {count} الصور مع علامة مختارة", - "countMore": "Top {count} images with the selected tag" - } - } - } - } + "flagStatus.nonexistent": "غير موجود", + "flagStatus.on": "على", + "flagStatus.off": "عن", + "footer.wordpressAffiliation": "جزء من مشروع {wordpress}", + "footer.wip": "🚧", + "language.language": "لغة", + "recentSearches.heading": "عمليات البحث الأخيرة", + "recentSearches.clear.text": "مسح", + "recentSearches.clear.label": "مسح عمليات البحث الأخيرة", + "recentSearches.clearSingle.label": "تم البحث مؤخراً عن '{entry}'", + "recentSearches.none": "لا توجد عمليات بحث حديثة لعرضها.", + "recentSearches.disclaimer": "لا يقوم Openverse بتخزين عمليات البحث الأخيرة ، يتم الاحتفاظ بهذه المعلومات محليًا في متصفحك.", + "report.imageDetails": "انظر تفاصيل الصورة", + "sensitiveContent.title.image": "هذه الصورة قد تحتوي على محتوى حساس", + "sensitiveContent.title.audio": "هذا المسار الصوتي قد يحتوي على محتوى حساس", + "sensitiveContent.creator": "Creator", + "sensitiveContent.singleResult.title": "المحتوى الحساس", + "sensitiveContent.singleResult.hide": "إخفاء المحتوى", + "sensitiveContent.singleResult.show": "محتوى العرض", + "sensitiveContent.singleResult.explanation": "وهذا العمل يتسم بالحساسية للأسباب التالية:", + "sensitiveContent.singleResult.learnMore": "{link} حول كيفية تعامل {openverse} مع المحتوى الحساس.", + "sensitiveContent.singleResult.link": "تعلم المزيد", + "sensitiveContent.reasons.providerSuppliedSensitive": "وقد اعتبر مصدر هذا العمل أنه حساس.", + "sensitiveContent.reasons.sensitiveText": "{openverse} رصدت نصا يمكن أن يكون حساسا.", + "sensitiveContent.reasons.userReportedSensitive": "{openverse} Use have reported this work as sensitive.", + "collection.heading.tag": "الكلمات الدلالية", + "collection.heading.creator": "مبتدع", + "collection.heading.source": "منشأ", + "collection.pageTitle.tag.audio": "{tag}", + "collection.pageTitle.tag.image": "صور {tag}", + "collection.pageTitle.source.audio": "{source}", + "collection.pageTitle.source.image": "صور {source}", + "collection.link.source": "موقع مفتوح المصدر", + "collection.link.creator": "افتح صفحة المنشئ", + "collection.ariaLabel.creator.audio": "الملفات الصوتية بواسطة {creator}", + "collection.ariaLabel.creator.image": "الصور عن طريق {creator}", + "collection.ariaLabel.source.audio": "ملفات صوتية من {source}", + "collection.ariaLabel.source.image": "الصور عن طريق {source}", + "collection.ariaLabel.tag.audio": "الملفات الصوتية مع العلامة {tag}", + "collection.ariaLabel.tag.image": "الصور التي تحمل الوسم {tag}", + "collection.resultCountLabel.creator.audio.zero": "لا توجد ملفات صوتية من قبل هذا الصانع", + "collection.resultCountLabel.creator.audio.count": "{count}", + "collection.resultCountLabel.creator.audio.countMore": "على الملفات الصوتية من قبل هذا الصانع", + "collection.resultCountLabel.creator.image.zero": "لا صور من هذا الصانع", + "collection.resultCountLabel.creator.image.count": "{count} صورة من هذا المبتكر.", + "collection.resultCountLabel.creator.image.countMore": "على صور {count} بواسطة هذا الصانع.", + "collection.resultCountLabel.source.audio.zero": "No audio files provided by this source", + "collection.resultCountLabel.source.audio.count": "{count} audio file provided by this source. {count}", + "collection.resultCountLabel.source.audio.countMore": "Top {count} audio files provided by this source", + "collection.resultCountLabel.source.image.zero": "No images provided by this source", + "collection.resultCountLabel.source.image.count": "{count} image provided by this source. {count} images provided by this source", + "collection.resultCountLabel.source.image.countMore": "Top {count} images provided by this source", + "collection.resultCountLabel.tag.audio.zero": "لا ملفات صوتية مع علامة مختارة", + "collection.resultCountLabel.tag.audio.count": "{count} {count}", + "collection.resultCountLabel.tag.audio.countMore": "Top {count} audio files with the selected tag", + "collection.resultCountLabel.tag.image.zero": "لا صور مع العلامة المختارة", + "collection.resultCountLabel.tag.image.count": "{count} {count} الصور مع علامة مختارة", + "collection.resultCountLabel.tag.image.countMore": "Top {count} images with the selected tag" } diff --git a/frontend/test/locales/es.json b/frontend/test/locales/es.json index df793806466..dbcd6d18757 100644 --- a/frontend/test/locales/es.json +++ b/frontend/test/locales/es.json @@ -1,949 +1,561 @@ { - "404": { - "title": "El contenido que estás buscando parece haber desaparecido.", - "main": "Ve a {link} o busca algo similar en el siguiente campo." - }, - "hero": { - "subtitle": "Explora entre más de 600 millones de elementos para reutilizar", - "description": "Una extensa biblioteca de fotos, imágenes y audio de stock gratuito, disponible para uso gratuito.", - "search": { - "placeholder": "Buscar contenidos" - }, - "disclaimer": { - "content": "Todo el contenido de {openverse} está bajo una {license} o es de dominio público.", - "license": "Licencia Creative Commons" - } - }, - "notification": { - "translation": { - "text": "La traducción para el idioma local {locale} está incompleta. Ayúdanos a alcanzar el 100 % {link}.", - "link": "contribuyendo en la traducción", - "close": "Cerrar la contribución de la traducción ayuda a solicitar banner" - }, - "analytics": { - "text": "Openverse utiliza análisis para mejorar la calidad de nuestro servicio. Visite {link} para aprender más o optar por salir.", - "link": "la página de privacidad", - "close": "Cierra el banner de los análisis." - } - }, - "header": { - "homeLink": "{openverse} Home", - "placeholder": "Buscar todos los contenidos", - "aria": { - "primary": "principal", - "menu": "menú", - "search": "buscar", - "srSearch": "botón de búsqueda" - }, - "aboutTab": "Acerca de", - "resourcesTab": "Recursos", - "loading": "Cargando...", - "filterButton": { - "simple": "Filtros", - "withCount": "{count} filtro|{count} filtros" - }, - "seeResults": "Ver resultados", - "backButton": "Vuelve.", - "contentSettingsButton": { - "simple": "Menú", - "withCount": "Menu. Filtro {count} aplicado en la práctica. Filtros {count} aplicados" - } - }, - "navigation": { - "about": "Acerca de", - "licenses": "Licencias", - "getInvolved": "Participar", - "api": "API", - "terms": "Términos", - "privacy": "Privacidad", - "feedback": "Feedback", - "sources": "Fuentes", - "externalSources": "Fuentes externas", - "searchHelp": "Buscar ayuda" - }, - "about": { - "title": "Sobre {openverse}", - "description": { - "content": "{openverse} es una herramienta que permite descubrir obras con licencia abierta y de dominio público y ser usadas por todos." - }, - "collection": { - "content": { - "a": "{openverse} busca más de 800 millones de imágenes y audio de API abiertas y el conjunto de datos {commonCrawl}", - "b": "Agregamos obras de múltiples repositorios públicos y facilitamos el reutilizamiento a través de características como la atribución de un clic." - } - }, - "planning": { - "content": { - "a": "Actualmente {openverse} sólo busca imágenes y audio, con búsqueda de vídeo proporcionado a través de Fuentes Externas.", - "b": "Planeamos añadir nuevos tipos de medios tales como textos abiertos y modelos 3D, con el objetivo final de proporcionar acceso a los aproximadamente 2.500 millones de obras de CC con licencia y dominio público en la web.", - "c": "Todo nuestro código es de código abierto y se puede acceder en el {repository} Nosotros {community} Puedes ver lo que {working}" - }, - "repository": "{openverse} Repositorio {github}", - "community": "agradecemos la colaboración de la comunidad", - "working": "estamos trabajando en este momento" - }, - "transfer": { - "content": { - "a": "{openverse} es el sucesor de CC Search que fue lanzado por Creative Commons en 2019, después de su migración a WordPress en 2021.", - "b": "Puede leer más sobre esta transición en los anuncios oficiales de {creativeCommons} y {wordpress}", - "c": "Seguimos comprometidos con nuestro objetivo de abordar la descubribilidad y accesibilidad de los medios de acceso abierto." - } - }, - "declaration": { - "content": { - "a": "{openverse} no verifica la información de licencia para trabajos individuales, o si la atribución generada es exacta o completa.", - "b": "Sírvase verificar independientemente el estado de licencia y la información de atribución antes de reutilizar el contenido. Para más detalles, lea el {terms}" - }, - "terms": "Términos de uso de {openverse}" - } - }, - "sources": { - "title": "Fuentes", - "detail": "Hacer clic en una {singleName} te permite explorar y filtrar los elementos dentro de esa fuente.", - "singleName": "Fuente", - "providers": { - "source": "Fuente", - "domain": "Dominio", - "item": "Elementos totales" - }, - "ccContent": { - "where": "¿De dónde proceden los contenidos de {openverse}?", - "content": "Hay contenidos con licencia abierta alojados en millones de dominios en toda la extensión de Internet. Nuestro equipo identifica sistemáticamente a los proveedores que alojan contenidos con licencia CC. Si son adecuados, los indexamos y los hacemos accesibles a través de {openverse}.", - "provider": { - "a": "Algunos proveedores tienen múltiples agrupaciones diferentes de contenido dentro de ellos. {flickr} tiene fuentes que van desde la NASA a la fotografía personal. El {smithsonian} comprende una docena de colecciones diversas.", - "b": "Wikimedia Commons ejecuta la gama en términos de contenido, y es utilizado por varias galerías, bibliotecas, archivos y museos destacando algunas o todas sus colecciones digitalizadas." - }, - "europeana": "{openverse} agradece especialmente el trabajo de {link}, una organización que trabaja para digitalizar y hacer localizables las obras del patrimonio cultural en toda Europa. {openverse} es capaz de indexar cientos de valiosas fuentes a través de una única integración con la {linkApi}." - }, - "newContent": { - "next": "¿Cómo decidimos cuáles serán las siguientes fuentes a añadir?", - "integrate": "Tenemos una lista interminable de posibles fuentes para investigar antes de la integración. Nos hacemos preguntas como:", - "impact": " ¿Cuál es el impacto o la importancia de esta fuente para nuestros usuarios? Si existe dentro de un proveedor como Wikimedia Commons, ¿es valioso para nuestros usuarios poder filtrar por esta fuente directamente?", - "reuse": "¿Se muestra claramente la información sobre la licencia y la atribución para permitir una reutilización segura?", - "totalItems": "¿Cuántos nuevos elementos totales o nuevos tipos de elementos podemos aportar a nuestros usuarios a través de esta integración? Algunas fuentes son integraciones directas, mientras que otras pueden ser una fuente dentro de otra fuente." - }, - "suggestions": "Apreciamos sugerencias de nuevas fuentes por parte de nuestra comunidad de usuarios.", - "issueButton": "Sugiere una nueva fuente", - "aria": { - "table": "tabla de fuentes" - }, - "heading": { - "image": "Fuentes de imágenes", - "audio": "Fuentes de audio" - } - }, - "externalSourcesPage": { - "title": "Búsqueda meta", - "intro": "{openverse} está basado en un catálogo que indexa contenidos con licencia CC y de dominio público de fuentes seleccionadas. Más información sobre nuestras {link}.", - "link": "fuentes", - "license": { - "a": "Sin embargo, hay muchas fuentes de los medios de comunicación de dominio público y licenciados por CC que todavía no podemos incluir en la búsqueda de {openverse}", - "b": "Esto puede ser porque no ofrecen una API pública, o que nuestros colaboradores aún no han tenido tiempo de integrarlos en {openverse}", - "c": "Estas son fuentes valoradas y queremos asegurarnos de que usted es capaz de encontrar los mejores materiales de licencia abierta posible, independientemente de dónde se encuentren." - }, - "new": { - "title": "¿Puedo sugerir más fuentes para la búsqueda meta?", - "content": "¡Sí, por favor! Crea un «{issue}» en nuestro repositorio de GitHub o envíanos un {email} y dinos qué nuevas fuentes te gustaría ver incluidas.", - "issue": "issue", - "email": "correo electrónico" - }, - "why": { - "title": "¿Por qué habéis creado esto?", - "content": "Durante muchos años, CC ha ofrecido a sus usuarios un portal de búsqueda dedicado para buscar plataformas que tienen filtros de licencias CC incorporados. Estas plataformas incluían Europeana, Google Images, Flickr, Jamendo, Open Clip Art Library, SpinXpress, Wikimedia Commons, YouTube, ccMixter y SoundCloud. La experiencia de búsqueda tenía el siguiente aspecto:", - "new": { - "a": "Para los usuarios del sitio web de CC Meta Search, la función \"External Sources\" en {openverse} se verá familiarizada.", - "b": "El objetivo era asegurar que la funcionalidad no se pierda, pero se actualiza e incorpora dentro de nuestro nuevo motor de búsqueda de contenido de licencia abierta.", - "c": "Además, la característica \"Fuerzas externas\" se basa en esta funcionalidad, permitiéndonos añadir rápidamente nuevas fuentes externas a medida que las descubrimos, y apoyar nuevos tipos de contenido en el futuro." - }, - "ariaLabel": "sugerencias", - "feedbackSuggestions": "Esperamos que disfrutes y, si tienes sugerencias para mejorar, déjanos tu {feedback}.", - "feedbackLink": "sugerencias" - }, - "relationships": { - "a": "Esta funcionalidad también nos permite iniciar conversaciones y construir relaciones con fuentes que puedan ser incluidas en {openverse} en el futuro.", - "b": "Por último, también podemos ofrecer fuentes externas de tipos de medios que no incluimos en {openverse} aún, pero planeamos hacerlo." - }, - "explanation": "Usted puede encontrar enlaces a fuentes externas en la parte inferior de cada {openverse} search results page; on pages for searches which return no results; and on pages for media types we do not yet support but intend to." - }, - "privacy": { - "title": "Privacidad", - "intro": { - "content": "El proyecto {openverse} busca dar prioridad a la privacidad y seguridad de nuestros usuarios. {openverse} se adhiere al {link} Vea este documento para una descripción completa de cómo {openverse} utiliza y protege cualquier información que nos proporcione.", - "link": "política de privacidad de todos los sitios web de WordPress.org" - }, - "cookies": { - "title": "Cookies", - "content": { - "a": "{openverse} utiliza cookies para almacenar información sobre las preferencias de los visitantes e información sobre sus navegadores web. Utilizamos esta información para mejorar la experiencia de usuario del sitio.", - "b": "Estas son consideradas \"necesarias\" o \"cookies estrictamente necesarias\". Puede deshabilitarlos cambiando la configuración de su navegador, pero esto puede afectar cómo funciona {openverse}" - } - }, - "contact": { - "title": "Contáctenos", - "content": "Cualquier pregunta sobre {openverse} y privacidad puede ser enviada a {email}, compartida como {issue}, o discutida con nuestra comunidad en el canal #openverso del {chat}", - "issue": "Cuestión de GitHub", - "chat": "Hacer una palabra Chat de prensa" - } - }, - "searchGuide": { - "title": "Guía de sintaxis de {openverse}", - "intro": "Cuando buscas, puedes introducir símbolos especiales o palabras a tu término de búsqueda para hacer que tus resultados de búsqueda sean más precisos.", - "exact": { - "title": "Buscar una coincidencia exacta", - "ariaLabel": "cita cerrar cita Claude Monet ", - "claudeMonet": "\"Claude Monet\"", - "content": "Pon una palabra dentro de comillas. Por ejemplo, {link}." - }, - "negate": { - "title": "Exclusión de los términos", - "operatorName": "menos operador", - "ariaLabel": "perro menos pug", - "example": "perro -pug", - "content": "Para excluir un término de sus resultados, ponga el {operator} delante de él. Ejemplo: {link}{br} Esto buscará medios relacionados con \"perro\" pero no incluirá resultados relacionados con \"pug\"." - } - }, - "feedback": { - "title": "Comentarios", - "intro": "¡Gracias por usar {openverse}! Agradecemos tus ideas para mejorar la herramienta. Para ofrecer sugerencias normales, únete al canal de {slack} en el espacio de trabajo {makingWordpress} de Slack.", - "improve": "Ayúdanos a mejorar", - "report": "Informar de un fallo", - "loading": "Cargando...", - "aria": { - "improve": "formulario para ayudarnos a mejorar", - "report": "formulario para informar de un fallo" - } - }, - "sensitive": { - "title": "Contenido sensible", - "description": { - "content": { - "a": "{openverse} opera a lo largo de un enfoque “seguro por defecto” en todos los aspectos de su funcionamiento y desarrollo, con la intención de ser lo más inclusivo y accesible posible.", - "b": "Por lo tanto, {openverse} solo incluye resultados con contenido sensible cuando los usuarios han optado explícitamente por las características de “incluye resultados sensibles” en {openverseOrg} y en el {openverse} API.", - "c": "En cumplimiento de {wpCoc} y su {deiStatement}, {openverse} aporta grandes expectativas en relación con la conducta hacia otros contribuyentes, la accesibilidad de la contribución y los servicios y, por lo tanto, siendo un proyecto inclusivo.", - "d": "Asimismo, {openverse} tiene la expectativa de que los resultados devueltos de la API o mostrados en el sitio web de {openverseOrg} deben ser accesibles por defecto.", - "e": "Todo el mundo, independientemente de su procedencia, debe sentirse seguro e incluido en {openverse}, ya sea que contribuya a los aspectos técnicos de los servicios de {openverse}, un creador cuyas obras están incluidas en {openverse}", - "f": "{openverse} reconoce su responsabilidad como herramienta utilizada por personas de una amplia variedad de edades, incluyendo jóvenes en entornos educativos, y presta especial atención a minimizar la interacción accidental con o la exposición al contenido sensible." - }, - "wpCoc": "Código comunitario de conducta de WordPress", - "deiStatement": "Declaración de diversidad, equidad e inclusión" - }, - "sensitivity": { - "what": { - "a": "{openverse} utiliza el término \"sensible\" en lugar de \"maduro\", \"NSFW\" (no seguro para el trabajo), u otros términos para indicar que nuestra designación de contenido tan sensible es amplia, con un enfoque en accesibilidad e inclusión.", - "b": "Esto significa que algún contenido es designado \"sensible\" que no caería en una categoría de lo que generalmente se entiende que es \"madura\" contenido (en otras palabras, contenido específicamente para un público adulto).", - "c": "La designación no implica, sin embargo, que {openverse} o sus sostenedores vean el contenido como inapropiado para la plataforma en general y no es igualmente una implicación del juicio moral o ético.", - "d": "Consideramos que el contenido \"sensible\" es un contenido ofensivo, perturbador, gráfico o inapropiado, prestando especial atención a los jóvenes." - }, - "how": { - "a": "Esta definición de sensibilidad tiene un tremendo grado de flexibilidad y es intencionalmente imprecisa.", - "b": "{openverse} se basa en una variedad de herramientas para descubrir contenido potencialmente sensible, incluyendo informes de usuarios moderados sobre trabajo individual y escanear el contenido textual relacionado con un trabajo para términos sensibles.", - "c": "Estos se describen con más detalle a continuación." - } - }, - "onOff": { - "title": "Activando y apagando contenidos sensibles", - "sensitiveResults": "Por defecto, {openverse} no incluye contenido sensible en los resultados de búsqueda. La inclusión de los resultados sensibles requiere un opt-in explícito del usuario. El usuario puede optar por incluir contenido sensible en los resultados de búsqueda permitiendo el interruptor de “Resultados positivos”.", - "blurSensitive": { - "a": "Cuando se incluye el contenido sensible, los resultados sensibles devueltos también están borrosos para evitar la exposición accidental.", - "b": "Desarreglarlos también requiere una opción explícita del usuario. El usuario puede optar por ver el contenido sensible no azulado desactivando el interruptor “Contenido azul”." - }, - "where": "Ambos toggles están disponibles en la barra lateral de filtro (en escritorios) y en la pestaña “Filter” del panel de configuración de búsqueda (en dispositivos móviles) en la página de resultados de búsqueda." - }, - "designations": { - "title": "Designaciones de contenido sensibles", - "description": { - "a": "{openverse} designa contenido sensible en la API y en el sitio web {openverseOrg} usando dos métodos: informes de usuarios de {openverse} y detección de contenidos textuales sensibles automatizada.", - "b": "Estas denominaciones no son exclusivas entre sí y un solo trabajo puede tener uno o ambos aplicados a él." - }, - "userReported": { - "title": "Sensibilidad comunicada por el usuario", - "description": { - "a": "Los usuarios de {openverse} son invitados a informar sobre contenidos sensibles a través del sitio web de {openverseOrg} y {openverse} API.", - "b": "Algunas herramientas y aplicaciones que se integran con la API {openverse}, como la API {gutenbergMediaInserter}, también permiten a sus usuarios reportar contenido sensible.", - "c": "La página de un trabajo individual incluye la capacidad de reportar contenido como sensible (o de denunciar violaciones de derechos).", - "d": "Los moderadores de {openverse} verifican estos informes y toman decisiones sobre si añadir una designación de sensibilidad al trabajo o, en ciertos casos como se describe anteriormente, deslistan el trabajo de {openverse}’s services." - }, - "gutenbergMediaInserter": "Editor de Gutenberg {openverse}" - }, - "sensitiveText": { - "title": "Contenido textual sensible", - "description": { - "a": "{openverse} escanea algunos de los metadatos textuales relacionados con las obras según nuestras fuentes para términos sensibles.", - "b": "{openverse} {sensitiveTermsList} es fuente abierta y las contribuciones y aportaciones de la comunidad son bienvenidas e invitadas.", - "c": "Ejemplos de textos potencialmente sensibles incluyen, pero no se limitan al texto de una naturaleza sexual, biológica, violenta, racista o de otro modo despectiva.", - "d": "El proyecto reconoce que este enfoque es imperfecto y que algunos trabajos pueden recibir inadvertidamente una designación de sensibilidad sin ser necesariamente sensibles.", - "e": "Para más contexto sobre por qué hemos elegido este enfoque a pesar de eso, consulte el {imperfect} de nuestro documento de planificación de proyectos relacionado con esta característica." - }, - "sensitiveTermsList": "lista de términos sensibles", - "imperfect": "Sección \"{sectionName}\"", - "metadata": { - "a": "Es importante señalar que algunos metadatos textuales para un trabajo son {notAvailable} a través de la API de {openverse} o el sitio web de {openverseOrg}", - "b": "Sin embargo, esos metadatos siguen siendo escaneados para términos sensibles y no se tratan como un caso especial.", - "c": "Si el escaneo de texto de {openverse} encuentra términos sensibles en esos campos de metadatos para un trabajo, el trabajo seguirá recibiendo una designación de sensibilidad basada en texto sensible aunque el texto sensible en sí mismo no esté disponible a través de {openverse}", - "d": "{openverse} toma el enfoque de que el contenido textual sensible en una descripción es un indicador correlativo relativamente alto de obras potencialmente sensibles.", - "e": "Como arriba, {openverse} entiende que esto no es perfecto." - }, - "notAvailable": "no disponible" - } - }, - "faq": { - "title": "Preguntas frecuentes", - "one": { - "question": "He encontrado contenido Creo que es sensible que no tiene una designación de sensibilidad. ¿Qué debo hacer?", - "answer": { - "a": "Por favor reporte contenido sensible visitando la página de trabajo individual en el sitio web de {openverseOrg} y utilizando el botón “reportar este contenido” debajo de la información de atribución y por encima de las etiquetas.", - "b": "{openverse} informes moderados individualmente y se reserva el derecho a rechazar respetuosamente la solicitud de añadir una designación de sensibilidad a un determinado trabajo." - } - }, - "two": { - "question": "No estoy de acuerdo con la designación de sensibilidad en un trabajo. ¿Puedes, por favor, quitarlo?", - "answerPt1": "Para designaciones basadas en textos, {openverse} no tiene en este momento un método para eliminar la designación. Esta es una característica que se construirá eventualmente, pero no es parte de la característica de detección de contenidos sensibles a la base.", - "answerPt2": { - "a": "Para los usuarios notificados, por favor presente un nuevo informe sobre la página del trabajo siguiendo las instrucciones en la pregunta anterior.", - "b": "En las notas, describa por qué cree que el trabajo no debe tener una designación de sensibilidad.", - "c": "Al agregar una nueva designación, {openverse} se reserva el derecho a rechazar respetuosamente la solicitud de eliminar una designación de sensibilidad de usuario confirmada." - } - }, - "three": { - "question": "He encontrado contenido en {openverse} que puede ser ilegal. Además de informarlo a {openverse}, ¿hay otros pasos que pueda tomar?", - "answer": { - "a": "Para los usuarios notificados, por favor presente un nuevo informe sobre la página del trabajo siguiendo las instrucciones en la pregunta anterior.", - "b": "En las notas, describa por qué cree que el trabajo no debe tener una designación de sensibilidad.", - "c": "Al agregar una nueva designación, {openverse} se reserva el derecho a rechazar respetuosamente la solicitud de eliminar una designación de sensibilidad de usuario confirmada." - } - } - } - }, - "tags": { - "title": "Comprender las etiquetas en {openverse}", - "intro": { - "a": "Cada trabajo creativo en {openverse} puede tener etiquetas, un conjunto opcional de palabras clave usadas para describir el trabajo y hacer más fácil para los usuarios encontrar los medios pertinentes para sus búsquedas.", - "b": "Estas etiquetas caen en dos categorías principales: etiquetas de origen y etiquetas generadas. Comprender la diferencia entre ellos puede mejorar su experiencia de búsqueda y mejorar la precisión de sus resultados." - }, - "sourceTags": { - "title": "Fuentes", - "content": { - "a": "Las etiquetas fuente son etiquetas que se originan de la fuente original del trabajo creativo. Estas etiquetas pueden ser agregadas por diferentes colaboradores, por ejemplo un fotógrafo que subió su imagen a Flickr y agregó etiquetas descriptivas.", - "b": "La plataforma original puede asignar etiquetas adicionales de miembros de la comunidad, automatización u otras fuentes." - } - }, - "generatedTags": { - "title": "Etiquetas generadas", - "content": { - "a": "Las etiquetas generadas se crean a través del análisis automático de las obras creativas, más comúnmente las imágenes. Este proceso implica tecnologías avanzadas como AWS Rekognition, Clarifai y otros servicios de reconocimiento de imágenes que analizan el contenido y generan etiquetas descriptivas.", - "b": "Si bien los sistemas generalmente fiables, automatizados a veces pueden malinterpretar o perder elementos en una imagen.", - "c": "Openverse hace esfuerzos para excluir cualquier etiqueta generada que haga inferencias sobre las identidades o afiliaciones de sujetos humanos.", - "d": "Si encuentras imágenes con etiquetas generadas haciendo suposiciones sobre, por ejemplo, género, religión o afiliación política, por favor reporta las imágenes usando el botón \"Informe\" en nuestras páginas de resultados individuales." - } - } - }, - "error": { - "occurred": "Ha ocurrido un error", - "imageNotFound": "No se ha podido encontrar la imagen con ID {id}", - "mediaNotFound": "No se ha podido encontrar {mediaType} con ID {id}", - "image": "imagen", - "audio": "audio" - }, - "filters": { - "title": "Filtros", - "filterBy": "Filtrar por", - "licenses": { - "title": "Licencias", - "cc0": "CC0", - "pdm": "Marca de dominio público", - "by": "BY", - "bySa": "BY-SA", - "byNc": "BY-NC", - "byNd": "BY-ND", - "byNcSa": "BY-NC-SA", - "byNcNd": "BY-NC-ND" - }, - "licenseTypes": { - "title": "Uso", - "commercial": "Usar comercialmente", - "modification": "Modificar o adaptar" - }, - "imageProviders": { - "title": "Fuente" - }, - "audioProviders": { - "title": "Fuente" - }, - "audioCategories": { - "title": "Categoría de audio", - "audiobook": "Audiolibro", - "music": "Música", - "news": "Noticias", - "podcast": "Podcast", - "pronunciation": "Pronunciación", - "sound_effect": "Efectos de sonido", - "sound": "Efectos de sonido" - }, - "imageCategories": { - "title": "Tipo de imagen", - "photograph": "Fotografías", - "illustration": "Ilustraciones", - "digitized_artwork": "Obras de arte digitalizadas" - }, - "audioExtensions": { - "title": "Tipo de archivo", - "flac": "FLAC", - "mid": "MID", - "mp3": "MP3", - "oga": "OGA", - "ogg": "OGG", - "opus": "OPUS", - "wav": "WAV", - "webm": "WEBM" - }, - "imageExtensions": { - "title": "Tipo de archivo", - "jpg": "JPEG", - "png": "PNG", - "gif": "GIF", - "svg": "SVG" - }, - "aspectRatios": { - "title": "Relación de aspecto", - "tall": "Alargada", - "wide": "Panorámica", - "square": "Cuadrada" - }, - "sizes": { - "title": "Tamaño de la imagen", - "small": "Pequeño", - "medium": "Medio", - "large": "Grande" - }, - "safeBrowsing": { - "title": "Cuervos seguros", - "desc": "El contenido marcado como {sensitive} no se muestra por defecto.", - "sensitive": "sensible", - "toggles": { - "fetchSensitive": { - "title": "Resultados sensibles", - "desc": "Mostrar resultados marcados como sensibles en el área de resultados." - }, - "blurSensitive": { - "title": "Contenido de Blur", - "desc": "Blur imágenes y textos para evitar ver material sensible." - } - } - }, - "lengths": { - "title": "Duración", - "shortest": "c) 30 segundos", - "short": "30 sec-2 min", - "medium": "2-10 min", - "long": "■ 10 min" - }, - "creator": { - "title": "Buscar por creador" - }, - "searchBy": { - "title": "Buscar por", - "creator": "Creador" - }, - "licenseExplanation": { - "licenseDefinition": "Definición de la licencia", - "markDefinition": "Definición de {mark}", - "more": { - "license": "{readMore} sobre esta licencia.", - "mark": "{readMore} sobre {mark}.", - "readMore": "Leer más" - } - }, - "aria": { - "removeFilter": "Eliminar el filtro {label}" - } - }, - "filterList": { - "filterBy": "Filtrar por", - "hide": "Ocultar los filtros", - "clear": "Vaciar los filtros", - "clearNumbered": "Filtros claros ({number})", - "show": "Mostrar los resultados", - "categoryAria": "lista de filtros para la categoría {categoryName}" - }, - "browsePage": { - "allNoResults": "No hay resultados", - "allResultCount": "{localeCount} resultado|{localeCount} resultados", - "allResultCountMore": "Más de {localeCount} resultado|Más de {localeCount} resultados", - "contentLink": { - "image": { - "zero": "No se han encontrado imágenes para {query}", - "count": "Véase {localeCount} imagen encontrada para {query} Ver imágenes {localeCount} encontradas para {query}", - "countMore": "Ver más imágenes de {localeCount} encontradas para {query}" - }, - "audio": { - "zero": "No se encuentra audio para {query}", - "count": "Ver {localeCount} audio encontrado para {query}", - "countMore": "Ver más {localeCount} audio encontrado para {query}" - } - }, - "load": "Cargar más resultados", - "loading": "Cargando...", - "fetchingError": "Error al recuperar {type}:", - "searchRating": { - "content": "¿Son relevantes estos resultados?", - "yes": "Sí", - "no": "No", - "feedbackThanks": "¡Gracias por tu opinión!" - }, - "searchForm": { - "placeholder": "Buscar todo {type}", - "image": "imágenes", - "audio": "audio", - "video": "vídeos", - "model3d": "Modelos 3D", - "all": "contenido", - "collectionPlaceholder": "Busca en esta colección", - "button": "Buscar", - "clear": "Despejado" - }, - "licenseDescription": { - "title": "Licencia CC", - "by": "Reconocer al creador.", - "nc": "Solo para uso no comercial.", - "nd": "No se permiten derivados ni adaptaciones.", - "sa": "Comparte las adaptaciones bajo los mismos términos.", - "zero": "Este trabajo ha sido marcado como dedicado al dominio público.", - "pd": "Este trabajo está marcado como de dominio público.", - "samplingPlus": "Se permiten muestras, integraciones y transformaciones creativas." - }, - "aria": { - "close": "cerrar", - "scroll": "scroll al inicio", - "search": "buscar", - "removeFilter": "eliminar el filtro", - "licenseExplanation": "explicación de la licencia", - "creator": "buscar por creador", - "imageTitle": "Imagen: {title}", - "audioTitle": "Audio: {title}", - "resultsLabel": { - "all": "Todos los resultados para {query}", - "image": "Resultados de la imagen para {query}", - "audio": "Audio tracks for {query}" - }, - "results": { - "all": "Todos los resultados para \"{query}\", {imageResults} y {audioResults}", - "image": { - "zero": "No hay resultados de imagen para \"{query}\"", - "count": "{localeCount} resultado de la imagen para \"{query}\" {localeCount} resultados de la imagen para \"{query}\"", - "countMore": "Los resultados de la imagen de {localeCount} para \"{query}\"." - }, - "audio": { - "zero": "No hay pistas de audio para \"{query}\"", - "count": "{localeCount} audio track for \"{query}\" {localeCount} audio tracks for \"{query}\"", - "countMore": "Top {localeCount} audio tracks for \"{query}\"." - } - }, - "allResultsHeadingCount": { - "image": { - "zero": "no imágenes", - "count": "{localeCount} image preserve{localeCount} images", - "countMore": "superior {localeCount} images" - }, - "audio": { - "zero": "sin pistas de audio", - "count": "{localeCount} audio track {localeCount} audio tracks", - "countMore": "superior {localeCount} audio tracks" - } - } - } - }, - "mediaDetails": { - "information": { - "type": "Tipo", - "unknown": "Desconocida", - "category": "Categoría" - }, - "scroll": { - "forward": "Desplazamiento hacia adelante", - "back": "Desplazarse hacia atrás" - }, - "reuse": { - "title": "Reutilizar el contenido", - "description": "Visite el sitio web de {media} para descargarlo y utilizarlo. Asegúrese de acreditar al creador mostrando la información de atribución donde está compartiendo su trabajo.", - "copyrightDisclaimer": "Algunas fotografías pueden contener contenido de copyright, como pinturas, esculturas o obras arquitectónicas. Utilizar estas fotografías puede requerir permisos adicionales del titular de derechos de autor de los trabajos representados.", - "licenseHeader": "Licencia", - "toolHeader": "Dominio público", - "audio": "Audio", - "image": "Imagen", - "tool": { - "content": "Lee más sobre la herramienta {link}.", - "link": "aquí" - }, - "credit": { - "genericTitle": "Esta obra", - "actualTitle": "«{title}»", - "text": "{title} {creator} {markedLicensed} {license}. {viewLegal}", - "creatorText": "por {creatorName}", - "marked": "está marcado con", - "licensed": "está bajo la licencia", - "viewLegalText": "Para ver los {termsCopy}, visita {url}.", - "termsText": "los términos", - "copyText": "una copia de esta licencia" - }, - "copyLicense": { - "title": "Reconocer al creador", - "rich": "Texto enriquecido", - "html": "HTML", - "plain": "Texto sin formato", - "copyText": "Copiar el texto", - "copied": "¡Copiado!", - "xml": "XML" - }, - "attribution": "Esta imagen se ha marcado con una licencia {link}:" - }, - "providerLabel": "Proveedor", - "sourceLabel": "Fuente", - "providerDescription": "Sitio web donde se hospeda el contenido", - "sourceDescription": "Organización que creó o posee el contenido original", - "loading": "Cargando...", - "relatedError": "Error al recuperar los medios relacionados", - "aria": { - "attribution": { - "license": "lee más sobre la licencia", - "tool": "lee más sobre la herramienta" - }, - "creatorUrl": "autor {name}" - }, - "imageInfo": "Información de la imagen", - "audioInfo": "Información del audio", - "tags": { - "title": "Etiquetas", - "generated": { - "heading": "Etiquetas generadas", - "pageTitle": "Más información" - }, - "source": { - "heading": "Etiquetas de la fuente" - }, - "showMore": "Mostrar más", - "showLess": "Mostrar menos" - }, - "contentReport": { - "short": "Informar", - "long": "Informar de este contenido", - "form": { - "disclaimer": "Por motivos de seguridad, {openverse} recopila y retiene direcciones IP anónimas de los que completan y envían este formulario.", - "question": "¿Cuál es la razón?", - "dmca": { - "option": "Infringe el copyright", - "note": "Debes rellenar este {form} para informar de la infracción del copyright. No se realizará ninguna acción hasta que este formulario se haya rellenado y enviado. Recomendamos hacer lo mismo en la fuente, {source}.", - "form": "Formulario DMCA", - "open": "Abrir el formulario" - }, - "sensitive": { - "option": "Contiene contenido sensible", - "subLabel": "Facultativo", - "placeholder": "Opcionalmente, proporcione una descripción." - }, - "other": { - "option": "Otra", - "note": "Describe el problema.", - "subLabel": "Obligatorio", - "placeholder": "Por favor, introduce al menos 20 caracteres." - }, - "submit": "Informar", - "cancel": "Cancelar" - }, - "success": { - "title": "Informe enviado correctamente", - "note": "Gracias por informar de este contenido. Recomendamos hacer lo mismo en la fuente, {source}." - }, - "failure": { - "title": "No se ha podido enviar el informe", - "note": "Algo ha ido mal. Por favor, inténtalo de nuevo más tarde." - } - } - }, - "singleResult": { - "back": "Volver a los resultados de búsqueda" - }, - "imageDetails": { - "creator": "por {name}", - "weblink": "Ir a la web de la imagen", - "information": { - "dimensions": "Dimensiones", - "pixels": "píxeles", - "sizeInPixels": "{width} × {height} pixels" - }, - "relatedImages": "Imágenes relacionadas", - "aria": { - "creatorUrl": "Autor {creator}" - } - }, - "audioDetails": { - "genreLabel": "Género", - "relatedAudios": "Audios relacionados", - "table": { - "album": "Álbum", - "sampleRate": "Tasa de muestreo", - "filetype": "Formato", - "genre": "Género" - }, - "weblink": "Consigue este audio" - }, - "allResults": { - "snackbar": { - "text": "Pulse {spacebar} para jugar o parar la pista.", - "spacebar": "Spacebar" - } - }, - "audioResults": { - "snackbar": { - "text": "Pulse {spacebar} para jugar o parar, y {left} & {right} para buscar a través de la pista.", - "spacebar": "Spacebar", - "left": "←", - "right": "→" - } - }, - "externalSources": { - "caption": "{openverse} no indexa actualmente las fuentes listadas anteriormente, pero, a través de esta interfaz, ofrece un cómodo acceso a los servicios de búsqueda proporcionados por otras organizaciones independientes. {openverse} no tiene control sobre los resultados devueltos. No asumas que los resultados mostrados en este portal de búsqueda están bajo una licencia CC. Comprueba siempre que la obra está realmente bajo una licencia CC siguiendo el enlace. Si tienes dudas, debes contactar directamente con el titular del copyright o intentar contactar con el sitio donde has encontrado el contenido.", - "button": "Lista de fuentes", - "title": "Fuentes externas", - "card": { - "search": "¿No encuentras lo que buscas? Prueba fuentes adicionales.", - "caption": "Haz clic en una fuente de abajo para buscar directamente otras colecciones de imágenes con licencia CC. {break}Por favor, ten en cuenta que el uso de filtros no es compatible con la biblioteca de Open Clip Art o Nappy. " - }, - "form": { - "supportedTitle": "¿No encuentras lo que buscas? Prueba fuentes adicionales de {type}.", - "supportedTitleSm": "Búsqueda en fuentes externas" - } - }, - "browsers": { - "chrome": "Chrome", - "firefox": "Firefox", - "opera": "Opera", - "edge": "Edge" - }, - "waveform": { - "label": "Barra de búsqueda de audio", - "currentTime": "{time} segundo|{time} segundos" - }, - "audioThumbnail": { - "alt": "Cubierta para «{title}» de {creator}" - }, - "audioTrack": { - "ariaLabel": "{title} - Reproductor de audio", - "ariaLabelInteractive": "{title} - Reproductor de audio - Pulsa la barra de espacio para reproducir y pausar una muestra del audio", - "ariaLabelInteractiveSeekable": "Audio: {title} - reproductor interactivo - presione la barra espaciadora para reproducir y pausar una vista previa del audio; utilice las teclas de flecha izquierda y derecha para buscar a través de la pista.", - "messages": { - "err_aborted": "Has abortado la reproducción.", - "err_network": "Ha ocurrido un error de red.", - "err_decode": "No se ha podido decodificar el audio.", - "err_unallowed": "Reproducción no permitida.", - "err_unknown": "Ha ocurrido un error inesperado. Pruebe de nuevo en unos minutos o informe el tema si el problema persiste.", - "err_unsupported": "Este formato de audio no es compatible con tu navegador.", - "loading": "Cargando..." - }, - "creator": "por {creator}", - "close": "Cerrar el reproductor de audio" - }, - "playPause": { - "play": "Reproducir", - "pause": "Pausar", - "replay": "Volver a reproducir", - "loading": "Cargando" - }, - "search": { - "search": "Buscar", - "searchBarLabel": "Buscar contenido en {openverse}" - }, - "licenseReadableNames": { - "cc0": "Cero", - "pdm": "Marca de dominio público", - "by": "Reconocimiento", - "bySa": "Reconocimiento-CompartirIgual", - "byNc": "Reconocimiento-NoComercial", - "byNd": "Reconocimiento-SinObraDerivada", - "byNcSa": "Reconocimiento-NoComercial-CompartirIgual", - "byNcNd": "Reconocimiento-NoComercial-SinObraDerivada", - "sampling+": "Sampling Plus", - "ncSampling+": "Sampling Plus NoComercial" - }, + "404.title": "El contenido que estás buscando parece haber desaparecido.", + "404.main": "Ve a {link} o busca algo similar en el siguiente campo.", + "hero.subtitle": "Explora entre más de 600 millones de elementos para reutilizar", + "hero.description": "Una extensa biblioteca de fotos, imágenes y audio de stock gratuito, disponible para uso gratuito.", + "hero.search.placeholder": "Buscar contenidos", + "hero.disclaimer.content": "Todo el contenido de {openverse} está bajo una {license} o es de dominio público.", + "hero.disclaimer.license": "Licencia Creative Commons", + "notification.translation.text": "La traducción para el idioma local {locale} está incompleta. Ayúdanos a alcanzar el 100 % {link}.", + "notification.translation.link": "contribuyendo en la traducción", + "notification.translation.close": "Cerrar la contribución de la traducción ayuda a solicitar banner", + "notification.analytics.text": "Openverse utiliza análisis para mejorar la calidad de nuestro servicio. Visite {link} para aprender más o optar por salir.", + "notification.analytics.link": "la página de privacidad", + "notification.analytics.close": "Cierra el banner de los análisis.", + "header.homeLink": "{openverse} Home", + "header.placeholder": "Buscar todos los contenidos", + "header.aria.primary": "principal", + "header.aria.menu": "menú", + "header.aria.search": "buscar", + "header.aria.srSearch": "botón de búsqueda", + "header.aboutTab": "Acerca de", + "header.resourcesTab": "Recursos", + "header.loading": "Cargando...", + "header.filterButton.simple": "Filtros", + "header.filterButton.withCount": "{count} filtro|{count} filtros", + "header.seeResults": "Ver resultados", + "header.backButton": "Vuelve.", + "header.contentSettingsButton.simple": "Menú", + "header.contentSettingsButton.withCount": "Menu. Filtro {count} aplicado en la práctica. Filtros {count} aplicados", + "navigation.about": "Acerca de", + "navigation.licenses": "Licencias", + "navigation.getInvolved": "Participar", + "navigation.api": "API", + "navigation.terms": "Términos", + "navigation.privacy": "Privacidad", + "navigation.feedback": "Feedback", + "navigation.sources": "Fuentes", + "navigation.externalSources": "Fuentes externas", + "navigation.searchHelp": "Buscar ayuda", + "about.title": "Sobre {openverse}", + "about.description.content": "{openverse} es una herramienta que permite descubrir obras con licencia abierta y de dominio público y ser usadas por todos.", + "about.collection.content.a": "{openverse} busca más de 800 millones de imágenes y audio de API abiertas y el conjunto de datos {commonCrawl}", + "about.collection.content.b": "Agregamos obras de múltiples repositorios públicos y facilitamos el reutilizamiento a través de características como la atribución de un clic.", + "about.planning.content.a": "Actualmente {openverse} sólo busca imágenes y audio, con búsqueda de vídeo proporcionado a través de Fuentes Externas.", + "about.planning.content.b": "Planeamos añadir nuevos tipos de medios tales como textos abiertos y modelos 3D, con el objetivo final de proporcionar acceso a los aproximadamente 2.500 millones de obras de CC con licencia y dominio público en la web.", + "about.planning.content.c": "Todo nuestro código es de código abierto y se puede acceder en el {repository} Nosotros {community} Puedes ver lo que {working}", + "about.planning.repository": "{openverse} Repositorio {github}", + "about.planning.community": "agradecemos la colaboración de la comunidad", + "about.planning.working": "estamos trabajando en este momento", + "about.transfer.content.a": "{openverse} es el sucesor de CC Search que fue lanzado por Creative Commons en 2019, después de su migración a WordPress en 2021.", + "about.transfer.content.b": "Puede leer más sobre esta transición en los anuncios oficiales de {creativeCommons} y {wordpress}", + "about.transfer.content.c": "Seguimos comprometidos con nuestro objetivo de abordar la descubribilidad y accesibilidad de los medios de acceso abierto.", + "about.declaration.content.a": "{openverse} no verifica la información de licencia para trabajos individuales, o si la atribución generada es exacta o completa.", + "about.declaration.content.b": "Sírvase verificar independientemente el estado de licencia y la información de atribución antes de reutilizar el contenido. Para más detalles, lea el {terms}", + "about.declaration.terms": "Términos de uso de {openverse}", + "sources.title": "Fuentes", + "sources.detail": "Hacer clic en una {singleName} te permite explorar y filtrar los elementos dentro de esa fuente.", + "sources.singleName": "Fuente", + "sources.providers.source": "Fuente", + "sources.providers.domain": "Dominio", + "sources.providers.item": "Elementos totales", + "sources.ccContent.where": "¿De dónde proceden los contenidos de {openverse}?", + "sources.ccContent.content": "Hay contenidos con licencia abierta alojados en millones de dominios en toda la extensión de Internet. Nuestro equipo identifica sistemáticamente a los proveedores que alojan contenidos con licencia CC. Si son adecuados, los indexamos y los hacemos accesibles a través de {openverse}.", + "sources.ccContent.provider.a": "Algunos proveedores tienen múltiples agrupaciones diferentes de contenido dentro de ellos. {flickr} tiene fuentes que van desde la NASA a la fotografía personal. El {smithsonian} comprende una docena de colecciones diversas.", + "sources.ccContent.provider.b": "Wikimedia Commons ejecuta la gama en términos de contenido, y es utilizado por varias galerías, bibliotecas, archivos y museos destacando algunas o todas sus colecciones digitalizadas.", + "sources.ccContent.europeana": "{openverse} agradece especialmente el trabajo de {link}, una organización que trabaja para digitalizar y hacer localizables las obras del patrimonio cultural en toda Europa. {openverse} es capaz de indexar cientos de valiosas fuentes a través de una única integración con la {linkApi}.", + "sources.newContent.next": "¿Cómo decidimos cuáles serán las siguientes fuentes a añadir?", + "sources.newContent.integrate": "Tenemos una lista interminable de posibles fuentes para investigar antes de la integración. Nos hacemos preguntas como:", + "sources.newContent.impact": " ¿Cuál es el impacto o la importancia de esta fuente para nuestros usuarios? Si existe dentro de un proveedor como Wikimedia Commons, ¿es valioso para nuestros usuarios poder filtrar por esta fuente directamente?", + "sources.newContent.reuse": "¿Se muestra claramente la información sobre la licencia y la atribución para permitir una reutilización segura?", + "sources.newContent.totalItems": "¿Cuántos nuevos elementos totales o nuevos tipos de elementos podemos aportar a nuestros usuarios a través de esta integración? Algunas fuentes son integraciones directas, mientras que otras pueden ser una fuente dentro de otra fuente.", + "sources.suggestions": "Apreciamos sugerencias de nuevas fuentes por parte de nuestra comunidad de usuarios.", + "sources.issueButton": "Sugiere una nueva fuente", + "sources.aria.table": "tabla de fuentes", + "sources.heading.image": "Fuentes de imágenes", + "sources.heading.audio": "Fuentes de audio", + "externalSourcesPage.title": "Búsqueda meta", + "externalSourcesPage.intro": "{openverse} está basado en un catálogo que indexa contenidos con licencia CC y de dominio público de fuentes seleccionadas. Más información sobre nuestras {link}.", + "externalSourcesPage.link": "fuentes", + "externalSourcesPage.license.a": "Sin embargo, hay muchas fuentes de los medios de comunicación de dominio público y licenciados por CC que todavía no podemos incluir en la búsqueda de {openverse}", + "externalSourcesPage.license.b": "Esto puede ser porque no ofrecen una API pública, o que nuestros colaboradores aún no han tenido tiempo de integrarlos en {openverse}", + "externalSourcesPage.license.c": "Estas son fuentes valoradas y queremos asegurarnos de que usted es capaz de encontrar los mejores materiales de licencia abierta posible, independientemente de dónde se encuentren.", + "externalSourcesPage.new.title": "¿Puedo sugerir más fuentes para la búsqueda meta?", + "externalSourcesPage.new.content": "¡Sí, por favor! Crea un «{issue}» en nuestro repositorio de GitHub o envíanos un {email} y dinos qué nuevas fuentes te gustaría ver incluidas.", + "externalSourcesPage.new.issue": "issue", + "externalSourcesPage.new.email": "correo electrónico", + "externalSourcesPage.why.title": "¿Por qué habéis creado esto?", + "externalSourcesPage.why.content": "Durante muchos años, CC ha ofrecido a sus usuarios un portal de búsqueda dedicado para buscar plataformas que tienen filtros de licencias CC incorporados. Estas plataformas incluían Europeana, Google Images, Flickr, Jamendo, Open Clip Art Library, SpinXpress, Wikimedia Commons, YouTube, ccMixter y SoundCloud. La experiencia de búsqueda tenía el siguiente aspecto:", + "externalSourcesPage.why.new.a": "Para los usuarios del sitio web de CC Meta Search, la función \"External Sources\" en {openverse} se verá familiarizada.", + "externalSourcesPage.why.new.b": "El objetivo era asegurar que la funcionalidad no se pierda, pero se actualiza e incorpora dentro de nuestro nuevo motor de búsqueda de contenido de licencia abierta.", + "externalSourcesPage.why.new.c": "Además, la característica \"Fuerzas externas\" se basa en esta funcionalidad, permitiéndonos añadir rápidamente nuevas fuentes externas a medida que las descubrimos, y apoyar nuevos tipos de contenido en el futuro.", + "externalSourcesPage.why.ariaLabel": "sugerencias", + "externalSourcesPage.why.feedbackSuggestions": "Esperamos que disfrutes y, si tienes sugerencias para mejorar, déjanos tu {feedback}.", + "externalSourcesPage.why.feedbackLink": "sugerencias", + "externalSourcesPage.relationships.a": "Esta funcionalidad también nos permite iniciar conversaciones y construir relaciones con fuentes que puedan ser incluidas en {openverse} en el futuro.", + "externalSourcesPage.relationships.b": "Por último, también podemos ofrecer fuentes externas de tipos de medios que no incluimos en {openverse} aún, pero planeamos hacerlo.", + "externalSourcesPage.explanation": "Usted puede encontrar enlaces a fuentes externas en la parte inferior de cada {openverse} search results page; on pages for searches which return no results; and on pages for media types we do not yet support but intend to.", + "privacy.title": "Privacidad", + "privacy.intro.content": "El proyecto {openverse} busca dar prioridad a la privacidad y seguridad de nuestros usuarios. {openverse} se adhiere al {link} Vea este documento para una descripción completa de cómo {openverse} utiliza y protege cualquier información que nos proporcione.", + "privacy.intro.link": "política de privacidad de todos los sitios web de WordPress.org", + "privacy.cookies.title": "Cookies", + "privacy.cookies.content.a": "{openverse} utiliza cookies para almacenar información sobre las preferencias de los visitantes e información sobre sus navegadores web. Utilizamos esta información para mejorar la experiencia de usuario del sitio.", + "privacy.cookies.content.b": "Estas son consideradas \"necesarias\" o \"cookies estrictamente necesarias\". Puede deshabilitarlos cambiando la configuración de su navegador, pero esto puede afectar cómo funciona {openverse}", + "privacy.contact.title": "Contáctenos", + "privacy.contact.content": "Cualquier pregunta sobre {openverse} y privacidad puede ser enviada a {email}, compartida como {issue}, o discutida con nuestra comunidad en el canal #openverso del {chat}", + "privacy.contact.issue": "Cuestión de GitHub", + "privacy.contact.chat": "Hacer una palabra Chat de prensa", + "searchGuide.title": "Guía de sintaxis de {openverse}", + "searchGuide.intro": "Cuando buscas, puedes introducir símbolos especiales o palabras a tu término de búsqueda para hacer que tus resultados de búsqueda sean más precisos.", + "searchGuide.exact.title": "Buscar una coincidencia exacta", + "searchGuide.exact.ariaLabel": "cita cerrar cita Claude Monet ", + "searchGuide.exact.claudeMonet": "\"Claude Monet\"", + "searchGuide.exact.content": "Pon una palabra dentro de comillas. Por ejemplo, {link}.", + "searchGuide.negate.title": "Exclusión de los términos", + "searchGuide.negate.operatorName": "menos operador", + "searchGuide.negate.ariaLabel": "perro menos pug", + "searchGuide.negate.example": "perro -pug", + "searchGuide.negate.content": "Para excluir un término de sus resultados, ponga el {operator} delante de él. Ejemplo: {link}{br} Esto buscará medios relacionados con \"perro\" pero no incluirá resultados relacionados con \"pug\".", + "feedback.title": "Comentarios", + "feedback.intro": "¡Gracias por usar {openverse}! Agradecemos tus ideas para mejorar la herramienta. Para ofrecer sugerencias normales, únete al canal de {slack} en el espacio de trabajo {makingWordpress} de Slack.", + "feedback.improve": "Ayúdanos a mejorar", + "feedback.report": "Informar de un fallo", + "feedback.loading": "Cargando...", + "feedback.aria.improve": "formulario para ayudarnos a mejorar", + "feedback.aria.report": "formulario para informar de un fallo", + "sensitive.title": "Contenido sensible", + "sensitive.description.content.a": "{openverse} opera a lo largo de un enfoque “seguro por defecto” en todos los aspectos de su funcionamiento y desarrollo, con la intención de ser lo más inclusivo y accesible posible.", + "sensitive.description.content.b": "Por lo tanto, {openverse} solo incluye resultados con contenido sensible cuando los usuarios han optado explícitamente por las características de “incluye resultados sensibles” en {openverseOrg} y en el {openverse} API.", + "sensitive.description.content.c": "En cumplimiento de {wpCoc} y su {deiStatement}, {openverse} aporta grandes expectativas en relación con la conducta hacia otros contribuyentes, la accesibilidad de la contribución y los servicios y, por lo tanto, siendo un proyecto inclusivo.", + "sensitive.description.content.d": "Asimismo, {openverse} tiene la expectativa de que los resultados devueltos de la API o mostrados en el sitio web de {openverseOrg} deben ser accesibles por defecto.", + "sensitive.description.content.e": "Todo el mundo, independientemente de su procedencia, debe sentirse seguro e incluido en {openverse}, ya sea que contribuya a los aspectos técnicos de los servicios de {openverse}, un creador cuyas obras están incluidas en {openverse}", + "sensitive.description.content.f": "{openverse} reconoce su responsabilidad como herramienta utilizada por personas de una amplia variedad de edades, incluyendo jóvenes en entornos educativos, y presta especial atención a minimizar la interacción accidental con o la exposición al contenido sensible.", + "sensitive.description.wpCoc": "Código comunitario de conducta de WordPress", + "sensitive.description.deiStatement": "Declaración de diversidad, equidad e inclusión", + "sensitive.sensitivity.what.a": "{openverse} utiliza el término \"sensible\" en lugar de \"maduro\", \"NSFW\" (no seguro para el trabajo), u otros términos para indicar que nuestra designación de contenido tan sensible es amplia, con un enfoque en accesibilidad e inclusión.", + "sensitive.sensitivity.what.b": "Esto significa que algún contenido es designado \"sensible\" que no caería en una categoría de lo que generalmente se entiende que es \"madura\" contenido (en otras palabras, contenido específicamente para un público adulto).", + "sensitive.sensitivity.what.c": "La designación no implica, sin embargo, que {openverse} o sus sostenedores vean el contenido como inapropiado para la plataforma en general y no es igualmente una implicación del juicio moral o ético.", + "sensitive.sensitivity.what.d": "Consideramos que el contenido \"sensible\" es un contenido ofensivo, perturbador, gráfico o inapropiado, prestando especial atención a los jóvenes.", + "sensitive.sensitivity.how.a": "Esta definición de sensibilidad tiene un tremendo grado de flexibilidad y es intencionalmente imprecisa.", + "sensitive.sensitivity.how.b": "{openverse} se basa en una variedad de herramientas para descubrir contenido potencialmente sensible, incluyendo informes de usuarios moderados sobre trabajo individual y escanear el contenido textual relacionado con un trabajo para términos sensibles.", + "sensitive.sensitivity.how.c": "Estos se describen con más detalle a continuación.", + "sensitive.onOff.title": "Activando y apagando contenidos sensibles", + "sensitive.onOff.sensitiveResults": "Por defecto, {openverse} no incluye contenido sensible en los resultados de búsqueda. La inclusión de los resultados sensibles requiere un opt-in explícito del usuario. El usuario puede optar por incluir contenido sensible en los resultados de búsqueda permitiendo el interruptor de “Resultados positivos”.", + "sensitive.onOff.blurSensitive.a": "Cuando se incluye el contenido sensible, los resultados sensibles devueltos también están borrosos para evitar la exposición accidental.", + "sensitive.onOff.blurSensitive.b": "Desarreglarlos también requiere una opción explícita del usuario. El usuario puede optar por ver el contenido sensible no azulado desactivando el interruptor “Contenido azul”.", + "sensitive.onOff.where": "Ambos toggles están disponibles en la barra lateral de filtro (en escritorios) y en la pestaña “Filter” del panel de configuración de búsqueda (en dispositivos móviles) en la página de resultados de búsqueda.", + "sensitive.designations.title": "Designaciones de contenido sensibles", + "sensitive.designations.description.a": "{openverse} designa contenido sensible en la API y en el sitio web {openverseOrg} usando dos métodos: informes de usuarios de {openverse} y detección de contenidos textuales sensibles automatizada.", + "sensitive.designations.description.b": "Estas denominaciones no son exclusivas entre sí y un solo trabajo puede tener uno o ambos aplicados a él.", + "sensitive.designations.userReported.title": "Sensibilidad comunicada por el usuario", + "sensitive.designations.userReported.description.a": "Los usuarios de {openverse} son invitados a informar sobre contenidos sensibles a través del sitio web de {openverseOrg} y {openverse} API.", + "sensitive.designations.userReported.description.b": "Algunas herramientas y aplicaciones que se integran con la API {openverse}, como la API {gutenbergMediaInserter}, también permiten a sus usuarios reportar contenido sensible.", + "sensitive.designations.userReported.description.c": "La página de un trabajo individual incluye la capacidad de reportar contenido como sensible (o de denunciar violaciones de derechos).", + "sensitive.designations.userReported.description.d": "Los moderadores de {openverse} verifican estos informes y toman decisiones sobre si añadir una designación de sensibilidad al trabajo o, en ciertos casos como se describe anteriormente, deslistan el trabajo de {openverse}’s services.", + "sensitive.designations.userReported.gutenbergMediaInserter": "Editor de Gutenberg {openverse}", + "sensitive.designations.sensitiveText.title": "Contenido textual sensible", + "sensitive.designations.sensitiveText.description.a": "{openverse} escanea algunos de los metadatos textuales relacionados con las obras según nuestras fuentes para términos sensibles.", + "sensitive.designations.sensitiveText.description.b": "{openverse} {sensitiveTermsList} es fuente abierta y las contribuciones y aportaciones de la comunidad son bienvenidas e invitadas.", + "sensitive.designations.sensitiveText.description.c": "Ejemplos de textos potencialmente sensibles incluyen, pero no se limitan al texto de una naturaleza sexual, biológica, violenta, racista o de otro modo despectiva.", + "sensitive.designations.sensitiveText.description.d": "El proyecto reconoce que este enfoque es imperfecto y que algunos trabajos pueden recibir inadvertidamente una designación de sensibilidad sin ser necesariamente sensibles.", + "sensitive.designations.sensitiveText.description.e": "Para más contexto sobre por qué hemos elegido este enfoque a pesar de eso, consulte el {imperfect} de nuestro documento de planificación de proyectos relacionado con esta característica.", + "sensitive.designations.sensitiveText.sensitiveTermsList": "lista de términos sensibles", + "sensitive.designations.sensitiveText.imperfect": "Sección \"{sectionName}\"", + "sensitive.designations.sensitiveText.metadata.a": "Es importante señalar que algunos metadatos textuales para un trabajo son {notAvailable} a través de la API de {openverse} o el sitio web de {openverseOrg}", + "sensitive.designations.sensitiveText.metadata.b": "Sin embargo, esos metadatos siguen siendo escaneados para términos sensibles y no se tratan como un caso especial.", + "sensitive.designations.sensitiveText.metadata.c": "Si el escaneo de texto de {openverse} encuentra términos sensibles en esos campos de metadatos para un trabajo, el trabajo seguirá recibiendo una designación de sensibilidad basada en texto sensible aunque el texto sensible en sí mismo no esté disponible a través de {openverse}", + "sensitive.designations.sensitiveText.metadata.d": "{openverse} toma el enfoque de que el contenido textual sensible en una descripción es un indicador correlativo relativamente alto de obras potencialmente sensibles.", + "sensitive.designations.sensitiveText.metadata.e": "Como arriba, {openverse} entiende que esto no es perfecto.", + "sensitive.designations.sensitiveText.notAvailable": "no disponible", + "sensitive.faq.title": "Preguntas frecuentes", + "sensitive.faq.one.question": "He encontrado contenido Creo que es sensible que no tiene una designación de sensibilidad. ¿Qué debo hacer?", + "sensitive.faq.one.answer.a": "Por favor reporte contenido sensible visitando la página de trabajo individual en el sitio web de {openverseOrg} y utilizando el botón “reportar este contenido” debajo de la información de atribución y por encima de las etiquetas.", + "sensitive.faq.one.answer.b": "{openverse} informes moderados individualmente y se reserva el derecho a rechazar respetuosamente la solicitud de añadir una designación de sensibilidad a un determinado trabajo.", + "sensitive.faq.two.question": "No estoy de acuerdo con la designación de sensibilidad en un trabajo. ¿Puedes, por favor, quitarlo?", + "sensitive.faq.two.answerPt1": "Para designaciones basadas en textos, {openverse} no tiene en este momento un método para eliminar la designación. Esta es una característica que se construirá eventualmente, pero no es parte de la característica de detección de contenidos sensibles a la base.", + "sensitive.faq.two.answerPt2.a": "Para los usuarios notificados, por favor presente un nuevo informe sobre la página del trabajo siguiendo las instrucciones en la pregunta anterior.", + "sensitive.faq.two.answerPt2.b": "En las notas, describa por qué cree que el trabajo no debe tener una designación de sensibilidad.", + "sensitive.faq.two.answerPt2.c": "Al agregar una nueva designación, {openverse} se reserva el derecho a rechazar respetuosamente la solicitud de eliminar una designación de sensibilidad de usuario confirmada.", + "sensitive.faq.three.question": "He encontrado contenido en {openverse} que puede ser ilegal. Además de informarlo a {openverse}, ¿hay otros pasos que pueda tomar?", + "sensitive.faq.three.answer.a": "Para los usuarios notificados, por favor presente un nuevo informe sobre la página del trabajo siguiendo las instrucciones en la pregunta anterior.", + "sensitive.faq.three.answer.b": "En las notas, describa por qué cree que el trabajo no debe tener una designación de sensibilidad.", + "sensitive.faq.three.answer.c": "Al agregar una nueva designación, {openverse} se reserva el derecho a rechazar respetuosamente la solicitud de eliminar una designación de sensibilidad de usuario confirmada.", + "tags.title": "Comprender las etiquetas en {openverse}", + "tags.intro.a": "Cada trabajo creativo en {openverse} puede tener etiquetas, un conjunto opcional de palabras clave usadas para describir el trabajo y hacer más fácil para los usuarios encontrar los medios pertinentes para sus búsquedas.", + "tags.intro.b": "Estas etiquetas caen en dos categorías principales: etiquetas de origen y etiquetas generadas. Comprender la diferencia entre ellos puede mejorar su experiencia de búsqueda y mejorar la precisión de sus resultados.", + "tags.sourceTags.title": "Fuentes", + "tags.sourceTags.content.a": "Las etiquetas fuente son etiquetas que se originan de la fuente original del trabajo creativo. Estas etiquetas pueden ser agregadas por diferentes colaboradores, por ejemplo un fotógrafo que subió su imagen a Flickr y agregó etiquetas descriptivas.", + "tags.sourceTags.content.b": "La plataforma original puede asignar etiquetas adicionales de miembros de la comunidad, automatización u otras fuentes.", + "tags.generatedTags.title": "Etiquetas generadas", + "tags.generatedTags.content.a": "Las etiquetas generadas se crean a través del análisis automático de las obras creativas, más comúnmente las imágenes. Este proceso implica tecnologías avanzadas como AWS Rekognition, Clarifai y otros servicios de reconocimiento de imágenes que analizan el contenido y generan etiquetas descriptivas.", + "tags.generatedTags.content.b": "Si bien los sistemas generalmente fiables, automatizados a veces pueden malinterpretar o perder elementos en una imagen.", + "tags.generatedTags.content.c": "Openverse hace esfuerzos para excluir cualquier etiqueta generada que haga inferencias sobre las identidades o afiliaciones de sujetos humanos.", + "tags.generatedTags.content.d": "Si encuentras imágenes con etiquetas generadas haciendo suposiciones sobre, por ejemplo, género, religión o afiliación política, por favor reporta las imágenes usando el botón \"Informe\" en nuestras páginas de resultados individuales.", + "error.occurred": "Ha ocurrido un error", + "error.imageNotFound": "No se ha podido encontrar la imagen con ID {id}", + "error.mediaNotFound": "No se ha podido encontrar {mediaType} con ID {id}", + "error.image": "imagen", + "error.audio": "audio", + "filters.title": "Filtros", + "filters.filterBy": "Filtrar por", + "filters.licenses.title": "Licencias", + "filters.licenses.cc0": "CC0", + "filters.licenses.pdm": "Marca de dominio público", + "filters.licenses.by": "BY", + "filters.licenses.bySa": "BY-SA", + "filters.licenses.byNc": "BY-NC", + "filters.licenses.byNd": "BY-ND", + "filters.licenses.byNcSa": "BY-NC-SA", + "filters.licenses.byNcNd": "BY-NC-ND", + "filters.licenseTypes.title": "Uso", + "filters.licenseTypes.commercial": "Usar comercialmente", + "filters.licenseTypes.modification": "Modificar o adaptar", + "filters.imageProviders.title": "Fuente", + "filters.audioProviders.title": "Fuente", + "filters.audioCategories.title": "Categoría de audio", + "filters.audioCategories.audiobook": "Audiolibro", + "filters.audioCategories.music": "Música", + "filters.audioCategories.news": "Noticias", + "filters.audioCategories.podcast": "Podcast", + "filters.audioCategories.pronunciation": "Pronunciación", + "filters.audioCategories.sound_effect": "Efectos de sonido", + "filters.audioCategories.sound": "Efectos de sonido", + "filters.imageCategories.title": "Tipo de imagen", + "filters.imageCategories.photograph": "Fotografías", + "filters.imageCategories.illustration": "Ilustraciones", + "filters.imageCategories.digitized_artwork": "Obras de arte digitalizadas", + "filters.audioExtensions.title": "Tipo de archivo", + "filters.audioExtensions.flac": "FLAC", + "filters.audioExtensions.mid": "MID", + "filters.audioExtensions.mp3": "MP3", + "filters.audioExtensions.oga": "OGA", + "filters.audioExtensions.ogg": "OGG", + "filters.audioExtensions.opus": "OPUS", + "filters.audioExtensions.wav": "WAV", + "filters.audioExtensions.webm": "WEBM", + "filters.imageExtensions.title": "Tipo de archivo", + "filters.imageExtensions.jpg": "JPEG", + "filters.imageExtensions.png": "PNG", + "filters.imageExtensions.gif": "GIF", + "filters.imageExtensions.svg": "SVG", + "filters.aspectRatios.title": "Relación de aspecto", + "filters.aspectRatios.tall": "Alargada", + "filters.aspectRatios.wide": "Panorámica", + "filters.aspectRatios.square": "Cuadrada", + "filters.sizes.title": "Tamaño de la imagen", + "filters.sizes.small": "Pequeño", + "filters.sizes.medium": "Medio", + "filters.sizes.large": "Grande", + "filters.safeBrowsing.title": "Cuervos seguros", + "filters.safeBrowsing.desc": "El contenido marcado como {sensitive} no se muestra por defecto.", + "filters.safeBrowsing.sensitive": "sensible", + "filters.safeBrowsing.toggles.fetchSensitive.title": "Resultados sensibles", + "filters.safeBrowsing.toggles.fetchSensitive.desc": "Mostrar resultados marcados como sensibles en el área de resultados.", + "filters.safeBrowsing.toggles.blurSensitive.title": "Contenido de Blur", + "filters.safeBrowsing.toggles.blurSensitive.desc": "Blur imágenes y textos para evitar ver material sensible.", + "filters.lengths.title": "Duración", + "filters.lengths.shortest": "c) 30 segundos", + "filters.lengths.short": "30 sec-2 min", + "filters.lengths.medium": "2-10 min", + "filters.lengths.long": "■ 10 min", + "filters.creator.title": "Buscar por creador", + "filters.searchBy.title": "Buscar por", + "filters.searchBy.creator": "Creador", + "filters.licenseExplanation.licenseDefinition": "Definición de la licencia", + "filters.licenseExplanation.markDefinition": "Definición de {mark}", + "filters.licenseExplanation.more.license": "{readMore} sobre esta licencia.", + "filters.licenseExplanation.more.mark": "{readMore} sobre {mark}.", + "filters.licenseExplanation.more.readMore": "Leer más", + "filters.aria.removeFilter": "Eliminar el filtro {label}", + "filterList.filterBy": "Filtrar por", + "filterList.hide": "Ocultar los filtros", + "filterList.clear": "Vaciar los filtros", + "filterList.clearNumbered": "Filtros claros ({number})", + "filterList.show": "Mostrar los resultados", + "filterList.categoryAria": "lista de filtros para la categoría {categoryName}", + "browsePage.allNoResults": "No hay resultados", + "browsePage.allResultCount": "{localeCount} resultado|{localeCount} resultados", + "browsePage.allResultCountMore": "Más de {localeCount} resultado|Más de {localeCount} resultados", + "browsePage.contentLink.image.zero": "No se han encontrado imágenes para {query}", + "browsePage.contentLink.image.count": "Véase {localeCount} imagen encontrada para {query} Ver imágenes {localeCount} encontradas para {query}", + "browsePage.contentLink.image.countMore": "Ver más imágenes de {localeCount} encontradas para {query}", + "browsePage.contentLink.audio.zero": "No se encuentra audio para {query}", + "browsePage.contentLink.audio.count": "Ver {localeCount} audio encontrado para {query}", + "browsePage.contentLink.audio.countMore": "Ver más {localeCount} audio encontrado para {query}", + "browsePage.load": "Cargar más resultados", + "browsePage.loading": "Cargando...", + "browsePage.fetchingError": "Error al recuperar {type}:", + "browsePage.searchRating.content": "¿Son relevantes estos resultados?", + "browsePage.searchRating.yes": "Sí", + "browsePage.searchRating.no": "No", + "browsePage.searchRating.feedbackThanks": "¡Gracias por tu opinión!", + "browsePage.searchForm.placeholder": "Buscar todo {type}", + "browsePage.searchForm.image": "imágenes", + "browsePage.searchForm.audio": "audio", + "browsePage.searchForm.video": "vídeos", + "browsePage.searchForm.model3d": "Modelos 3D", + "browsePage.searchForm.all": "contenido", + "browsePage.searchForm.collectionPlaceholder": "Busca en esta colección", + "browsePage.searchForm.button": "Buscar", + "browsePage.searchForm.clear": "Despejado", + "browsePage.licenseDescription.title": "Licencia CC", + "browsePage.licenseDescription.by": "Reconocer al creador.", + "browsePage.licenseDescription.nc": "Solo para uso no comercial.", + "browsePage.licenseDescription.nd": "No se permiten derivados ni adaptaciones.", + "browsePage.licenseDescription.sa": "Comparte las adaptaciones bajo los mismos términos.", + "browsePage.licenseDescription.zero": "Este trabajo ha sido marcado como dedicado al dominio público.", + "browsePage.licenseDescription.pd": "Este trabajo está marcado como de dominio público.", + "browsePage.licenseDescription.samplingPlus": "Se permiten muestras, integraciones y transformaciones creativas.", + "browsePage.aria.close": "cerrar", + "browsePage.aria.scroll": "scroll al inicio", + "browsePage.aria.search": "buscar", + "browsePage.aria.removeFilter": "eliminar el filtro", + "browsePage.aria.licenseExplanation": "explicación de la licencia", + "browsePage.aria.creator": "buscar por creador", + "browsePage.aria.imageTitle": "Imagen: {title}", + "browsePage.aria.audioTitle": "Audio: {title}", + "browsePage.aria.resultsLabel.all": "Todos los resultados para {query}", + "browsePage.aria.resultsLabel.image": "Resultados de la imagen para {query}", + "browsePage.aria.resultsLabel.audio": "Audio tracks for {query}", + "browsePage.aria.results.all": "Todos los resultados para \"{query}\", {imageResults} y {audioResults}", + "browsePage.aria.results.image.zero": "No hay resultados de imagen para \"{query}\"", + "browsePage.aria.results.image.count": "{localeCount} resultado de la imagen para \"{query}\" {localeCount} resultados de la imagen para \"{query}\"", + "browsePage.aria.results.image.countMore": "Los resultados de la imagen de {localeCount} para \"{query}\".", + "browsePage.aria.results.audio.zero": "No hay pistas de audio para \"{query}\"", + "browsePage.aria.results.audio.count": "{localeCount} audio track for \"{query}\" {localeCount} audio tracks for \"{query}\"", + "browsePage.aria.results.audio.countMore": "Top {localeCount} audio tracks for \"{query}\".", + "browsePage.aria.allResultsHeadingCount.image.zero": "no imágenes", + "browsePage.aria.allResultsHeadingCount.image.count": "{localeCount} image preserve{localeCount} images", + "browsePage.aria.allResultsHeadingCount.image.countMore": "superior {localeCount} images", + "browsePage.aria.allResultsHeadingCount.audio.zero": "sin pistas de audio", + "browsePage.aria.allResultsHeadingCount.audio.count": "{localeCount} audio track {localeCount} audio tracks", + "browsePage.aria.allResultsHeadingCount.audio.countMore": "superior {localeCount} audio tracks", + "mediaDetails.information.type": "Tipo", + "mediaDetails.information.unknown": "Desconocida", + "mediaDetails.information.category": "Categoría", + "mediaDetails.scroll.forward": "Desplazamiento hacia adelante", + "mediaDetails.scroll.back": "Desplazarse hacia atrás", + "mediaDetails.reuse.title": "Reutilizar el contenido", + "mediaDetails.reuse.description": "Visite el sitio web de {media} para descargarlo y utilizarlo. Asegúrese de acreditar al creador mostrando la información de atribución donde está compartiendo su trabajo.", + "mediaDetails.reuse.copyrightDisclaimer": "Algunas fotografías pueden contener contenido de copyright, como pinturas, esculturas o obras arquitectónicas. Utilizar estas fotografías puede requerir permisos adicionales del titular de derechos de autor de los trabajos representados.", + "mediaDetails.reuse.licenseHeader": "Licencia", + "mediaDetails.reuse.toolHeader": "Dominio público", + "mediaDetails.reuse.audio": "Audio", + "mediaDetails.reuse.image": "Imagen", + "mediaDetails.reuse.tool.content": "Lee más sobre la herramienta {link}.", + "mediaDetails.reuse.tool.link": "aquí", + "mediaDetails.reuse.credit.genericTitle": "Esta obra", + "mediaDetails.reuse.credit.actualTitle": "«{title}»", + "mediaDetails.reuse.credit.text": "{title} {creator} {markedLicensed} {license}. {viewLegal}", + "mediaDetails.reuse.credit.creatorText": "por {creatorName}", + "mediaDetails.reuse.credit.marked": "está marcado con", + "mediaDetails.reuse.credit.licensed": "está bajo la licencia", + "mediaDetails.reuse.credit.viewLegalText": "Para ver los {termsCopy}, visita {url}.", + "mediaDetails.reuse.credit.termsText": "los términos", + "mediaDetails.reuse.credit.copyText": "una copia de esta licencia", + "mediaDetails.reuse.copyLicense.title": "Reconocer al creador", + "mediaDetails.reuse.copyLicense.rich": "Texto enriquecido", + "mediaDetails.reuse.copyLicense.html": "HTML", + "mediaDetails.reuse.copyLicense.plain": "Texto sin formato", + "mediaDetails.reuse.copyLicense.copyText": "Copiar el texto", + "mediaDetails.reuse.copyLicense.copied": "¡Copiado!", + "mediaDetails.reuse.copyLicense.xml": "XML", + "mediaDetails.reuse.attribution": "Esta imagen se ha marcado con una licencia {link}:", + "mediaDetails.providerLabel": "Proveedor", + "mediaDetails.sourceLabel": "Fuente", + "mediaDetails.providerDescription": "Sitio web donde se hospeda el contenido", + "mediaDetails.sourceDescription": "Organización que creó o posee el contenido original", + "mediaDetails.loading": "Cargando...", + "mediaDetails.relatedError": "Error al recuperar los medios relacionados", + "mediaDetails.aria.attribution.license": "lee más sobre la licencia", + "mediaDetails.aria.attribution.tool": "lee más sobre la herramienta", + "mediaDetails.aria.creatorUrl": "autor {name}", + "mediaDetails.imageInfo": "Información de la imagen", + "mediaDetails.audioInfo": "Información del audio", + "mediaDetails.tags.title": "Etiquetas", + "mediaDetails.tags.generated.heading": "Etiquetas generadas", + "mediaDetails.tags.generated.pageTitle": "Más información", + "mediaDetails.tags.source.heading": "Etiquetas de la fuente", + "mediaDetails.tags.showMore": "Mostrar más", + "mediaDetails.tags.showLess": "Mostrar menos", + "mediaDetails.contentReport.short": "Informar", + "mediaDetails.contentReport.long": "Informar de este contenido", + "mediaDetails.contentReport.form.disclaimer": "Por motivos de seguridad, {openverse} recopila y retiene direcciones IP anónimas de los que completan y envían este formulario.", + "mediaDetails.contentReport.form.question": "¿Cuál es la razón?", + "mediaDetails.contentReport.form.dmca.option": "Infringe el copyright", + "mediaDetails.contentReport.form.dmca.note": "Debes rellenar este {form} para informar de la infracción del copyright. No se realizará ninguna acción hasta que este formulario se haya rellenado y enviado. Recomendamos hacer lo mismo en la fuente, {source}.", + "mediaDetails.contentReport.form.dmca.form": "Formulario DMCA", + "mediaDetails.contentReport.form.dmca.open": "Abrir el formulario", + "mediaDetails.contentReport.form.sensitive.option": "Contiene contenido sensible", + "mediaDetails.contentReport.form.sensitive.subLabel": "Facultativo", + "mediaDetails.contentReport.form.sensitive.placeholder": "Opcionalmente, proporcione una descripción.", + "mediaDetails.contentReport.form.other.option": "Otra", + "mediaDetails.contentReport.form.other.note": "Describe el problema.", + "mediaDetails.contentReport.form.other.subLabel": "Obligatorio", + "mediaDetails.contentReport.form.other.placeholder": "Por favor, introduce al menos 20 caracteres.", + "mediaDetails.contentReport.form.submit": "Informar", + "mediaDetails.contentReport.form.cancel": "Cancelar", + "mediaDetails.contentReport.success.title": "Informe enviado correctamente", + "mediaDetails.contentReport.success.note": "Gracias por informar de este contenido. Recomendamos hacer lo mismo en la fuente, {source}.", + "mediaDetails.contentReport.failure.title": "No se ha podido enviar el informe", + "mediaDetails.contentReport.failure.note": "Algo ha ido mal. Por favor, inténtalo de nuevo más tarde.", + "singleResult.back": "Volver a los resultados de búsqueda", + "imageDetails.creator": "por {name}", + "imageDetails.weblink": "Ir a la web de la imagen", + "imageDetails.information.dimensions": "Dimensiones", + "imageDetails.information.pixels": "píxeles", + "imageDetails.information.sizeInPixels": "{width} × {height} pixels", + "imageDetails.relatedImages": "Imágenes relacionadas", + "imageDetails.aria.creatorUrl": "Autor {creator}", + "audioDetails.genreLabel": "Género", + "audioDetails.relatedAudios": "Audios relacionados", + "audioDetails.table.album": "Álbum", + "audioDetails.table.sampleRate": "Tasa de muestreo", + "audioDetails.table.filetype": "Formato", + "audioDetails.table.genre": "Género", + "audioDetails.weblink": "Consigue este audio", + "allResults.snackbar.text": "Pulse {spacebar} para jugar o parar la pista.", + "allResults.snackbar.spacebar": "Spacebar", + "audioResults.snackbar.text": "Pulse {spacebar} para jugar o parar, y {left} & {right} para buscar a través de la pista.", + "audioResults.snackbar.spacebar": "Spacebar", + "audioResults.snackbar.left": "←", + "audioResults.snackbar.right": "→", + "externalSources.caption": "{openverse} no indexa actualmente las fuentes listadas anteriormente, pero, a través de esta interfaz, ofrece un cómodo acceso a los servicios de búsqueda proporcionados por otras organizaciones independientes. {openverse} no tiene control sobre los resultados devueltos. No asumas que los resultados mostrados en este portal de búsqueda están bajo una licencia CC. Comprueba siempre que la obra está realmente bajo una licencia CC siguiendo el enlace. Si tienes dudas, debes contactar directamente con el titular del copyright o intentar contactar con el sitio donde has encontrado el contenido.", + "externalSources.button": "Lista de fuentes", + "externalSources.title": "Fuentes externas", + "externalSources.card.search": "¿No encuentras lo que buscas? Prueba fuentes adicionales.", + "externalSources.card.caption": "Haz clic en una fuente de abajo para buscar directamente otras colecciones de imágenes con licencia CC. {break}Por favor, ten en cuenta que el uso de filtros no es compatible con la biblioteca de Open Clip Art o Nappy. ", + "externalSources.form.supportedTitle": "¿No encuentras lo que buscas? Prueba fuentes adicionales de {type}.", + "externalSources.form.supportedTitleSm": "Búsqueda en fuentes externas", + "browsers.chrome": "Chrome", + "browsers.firefox": "Firefox", + "browsers.opera": "Opera", + "browsers.edge": "Edge", + "waveform.label": "Barra de búsqueda de audio", + "waveform.currentTime": "{time} segundo|{time} segundos", + "audioThumbnail.alt": "Cubierta para «{title}» de {creator}", + "audioTrack.ariaLabel": "{title} - Reproductor de audio", + "audioTrack.ariaLabelInteractive": "{title} - Reproductor de audio - Pulsa la barra de espacio para reproducir y pausar una muestra del audio", + "audioTrack.ariaLabelInteractiveSeekable": "Audio: {title} - reproductor interactivo - presione la barra espaciadora para reproducir y pausar una vista previa del audio; utilice las teclas de flecha izquierda y derecha para buscar a través de la pista.", + "audioTrack.messages.err_aborted": "Has abortado la reproducción.", + "audioTrack.messages.err_network": "Ha ocurrido un error de red.", + "audioTrack.messages.err_decode": "No se ha podido decodificar el audio.", + "audioTrack.messages.err_unallowed": "Reproducción no permitida.", + "audioTrack.messages.err_unknown": "Ha ocurrido un error inesperado. Pruebe de nuevo en unos minutos o informe el tema si el problema persiste.", + "audioTrack.messages.err_unsupported": "Este formato de audio no es compatible con tu navegador.", + "audioTrack.messages.loading": "Cargando...", + "audioTrack.creator": "por {creator}", + "audioTrack.close": "Cerrar el reproductor de audio", + "playPause.play": "Reproducir", + "playPause.pause": "Pausar", + "playPause.replay": "Volver a reproducir", + "playPause.loading": "Cargando", + "search.search": "Buscar", + "search.searchBarLabel": "Buscar contenido en {openverse}", + "licenseReadableNames.cc0": "Cero", + "licenseReadableNames.pdm": "Marca de dominio público", + "licenseReadableNames.by": "Reconocimiento", + "licenseReadableNames.bySa": "Reconocimiento-CompartirIgual", + "licenseReadableNames.byNc": "Reconocimiento-NoComercial", + "licenseReadableNames.byNd": "Reconocimiento-SinObraDerivada", + "licenseReadableNames.byNcSa": "Reconocimiento-NoComercial-CompartirIgual", + "licenseReadableNames.byNcNd": "Reconocimiento-NoComercial-SinObraDerivada", + "licenseReadableNames.sampling+": "Sampling Plus", + "licenseReadableNames.ncSampling+": "Sampling Plus NoComercial", "interpunct": "•", - "modal": { - "close": "Cerrar", - "ariaClose": "Cerrar el modal", - "closeNamed": "Cerrar {name}", - "closeContentSettings": "Cerrar el menú de configuración del contenido", - "closePagesMenu": "Cerrar el menú de páginas", - "closeBanner": "Cierra la bandera" - }, - "errorImages": { - "depressedMusician": "Un pianista deprimido descansa la cabeza en sus manos.", - "waitingForABite": "Tres chicos sentados en un tronco roto mientras dos de ellos pescan." - }, - "noResults": { - "heading": "No hemos podido encontrar nada para «{query}».", - "alternatives": "Intenta una consulta diferente o usa una de las fuentes adicionales para ampliar tu búsqueda." - }, - "serverTimeout": { - "heading": "¡Vaya! Parece que esa petición tarda demasiado en completarse. Por favor, inténtalo de nuevo." - }, - "unknownError": { - "heading": "Parece que algo salió mal. Por favor, inténtalo de nuevo." - }, - "searchType": { - "image": "Imágenes", - "audio": "Audio", - "all": "Todo el contenido", - "video": "Vídeos", - "model3d": "Modelos 3D", - "label": "Tipo de contenido a buscar", - "heading": "Tipos de contenido", - "additional": "Fuentes adicionales", - "statusBeta": "Beta", - "seeImage": "Ver todas las imágenes", - "seeAudio": "Ver todos los audios", - "selectLabel": "Seleccione un tipo de contenido: {type}" - }, + "modal.close": "Cerrar", + "modal.ariaClose": "Cerrar el modal", + "modal.closeNamed": "Cerrar {name}", + "modal.closeContentSettings": "Cerrar el menú de configuración del contenido", + "modal.closePagesMenu": "Cerrar el menú de páginas", + "modal.closeBanner": "Cierra la bandera", + "errorImages.depressedMusician": "Un pianista deprimido descansa la cabeza en sus manos.", + "errorImages.waitingForABite": "Tres chicos sentados en un tronco roto mientras dos de ellos pescan.", + "noResults.heading": "No hemos podido encontrar nada para «{query}».", + "noResults.alternatives": "Intenta una consulta diferente o usa una de las fuentes adicionales para ampliar tu búsqueda.", + "serverTimeout.heading": "¡Vaya! Parece que esa petición tarda demasiado en completarse. Por favor, inténtalo de nuevo.", + "unknownError.heading": "Parece que algo salió mal. Por favor, inténtalo de nuevo.", + "searchType.image": "Imágenes", + "searchType.audio": "Audio", + "searchType.all": "Todo el contenido", + "searchType.video": "Vídeos", + "searchType.model3d": "Modelos 3D", + "searchType.label": "Tipo de contenido a buscar", + "searchType.heading": "Tipos de contenido", + "searchType.additional": "Fuentes adicionales", + "searchType.statusBeta": "Beta", + "searchType.seeImage": "Ver todas las imágenes", + "searchType.seeAudio": "Ver todos los audios", + "searchType.selectLabel": "Seleccione un tipo de contenido: {type}", "skipToContent": "Saltar al contenido", - "prefPage": { - "title": "Preferencias", - "groups": { - "analytics": { - "title": "Análisis", - "desc": "{openverse} utiliza análisis anónimos para mejorar nuestro servicio. No recopilamos ninguna información que pueda utilizarse para identificarte personalmente. Sin embargo, si desea no participar, puede optar aquí." - } - }, - "features": { - "analytics": "Grabar eventos personalizados y vistas de página para análisis." - }, - "nonSwitchable": { - "title": "Características que no se pueden cambiar.", - "desc": "No puedes modificar el estado de estas características." - }, - "switchable": { - "title": "Características que se pueden cambiar", - "desc": "Puedes activar o desactivar estas características a tu gusto y tus preferencias se guardarán en una cookie." - }, - "storeState": "Provincia de la tienda", - "contentFiltering": "Filtrado de contenido", - "explanation": "Mostrado porque {featName} es {featState}" - }, + "prefPage.title": "Preferencias", + "prefPage.groups.analytics.title": "Análisis", + "prefPage.groups.analytics.desc": "{openverse} utiliza análisis anónimos para mejorar nuestro servicio. No recopilamos ninguna información que pueda utilizarse para identificarte personalmente. Sin embargo, si desea no participar, puede optar aquí.", + "prefPage.features.analytics": "Grabar eventos personalizados y vistas de página para análisis.", + "prefPage.nonSwitchable.title": "Características que no se pueden cambiar.", + "prefPage.nonSwitchable.desc": "No puedes modificar el estado de estas características.", + "prefPage.switchable.title": "Características que se pueden cambiar", + "prefPage.switchable.desc": "Puedes activar o desactivar estas características a tu gusto y tus preferencias se guardarán en una cookie.", + "prefPage.storeState": "Provincia de la tienda", + "prefPage.contentFiltering": "Filtrado de contenido", + "prefPage.explanation": "Mostrado porque {featName} es {featState}", "sketchfabIframeTitle": "Visor {sketchfab}", - "flagStatus": { - "nonexistent": "Inexistentes", - "on": "On", - "off": "Fuera." - }, - "footer": { - "wordpressAffiliation": "Parte del proyecto {wordpress}", - "wip": "🚧" - }, - "language": { - "language": "Idioma" - }, - "recentSearches": { - "heading": "Búsquedas recientes", - "clear": { - "text": "Despejado", - "label": "Búsquedas recientes claras" - }, - "clearSingle": { - "label": "Clear recent search '{entry} '" - }, - "none": "No hay búsquedas recientes para mostrar.", - "disclaimer": "Openverse no almacena sus búsquedas recientes, esta información se mantiene localmente en su navegador." - }, - "report": { - "imageDetails": "Ver detalles de la imagen" - }, - "sensitiveContent": { - "title": { - "image": "Esta imagen puede contener contenido sensible.", - "audio": "Esta pista de audio puede contener contenido sensible." - }, - "creator": "Creador", - "singleResult": { - "title": "Contenido sensible", - "hide": "Ocultar contenido", - "show": "Mostrar contenido", - "explanation": "Este trabajo está marcado como sensible por las siguientes razones:", - "learnMore": "{link} sobre cómo {openverse} maneja contenido sensible.", - "link": "Aprender más" - }, - "reasons": { - "providerSuppliedSensitive": "La fuente de este trabajo lo ha marcado como sensible.", - "sensitiveText": "{openverse} ha detectado texto potencialmente sensible.", - "userReportedSensitive": "Los usuarios de {openverse} han reportado este trabajo como sensible." - } - }, - "collection": { - "heading": { - "tag": "Tag", - "creator": "Creador", - "source": "Fuente" - }, - "pageTitle": { - "tag": { - "audio": "{tag} audio tracks", - "image": "{tag} images" - }, - "source": { - "audio": "{source} audio tracks", - "image": "{source} images" - } - }, - "link": { - "source": "Sitio de código abierto", - "creator": "Página de creador abierta" - }, - "ariaLabel": { - "creator": { - "audio": "Archivos de audio de {creator} en {source}", - "image": "Images by {creator} in {source}" - }, - "source": { - "audio": "Archivos de audio de {source}", - "image": "Imágenes de {source}" - }, - "tag": { - "audio": "Archivos de audio con la etiqueta {tag}", - "image": "Imágenes con la etiqueta {tag}" - } - }, - "resultCountLabel": { - "creator": { - "audio": { - "zero": "No hay archivos de audio de este creador.", - "count": "{count} audio de este creador.|{count} archivos de audio de este creador.", - "countMore": "Sobre archivos de audio {count} de este creador." - }, - "image": { - "zero": "No hay imágenes de este creador.", - "count": "{count} imágen de este creador|{count} imágens de este creador.", - "countMore": "Sobre las imágenes de {count} de este creador." - } - }, - "source": { - "audio": { - "zero": "No hay archivos de audio proporcionados por esta fuente", - "count": "{count} archivo de audio proporcionado por esta fuente Archivos de audio {count} proporcionados por esta fuente", - "countMore": "Más archivos de audio {count} proporcionados por esta fuente" - }, - "image": { - "zero": "No hay imágenes proporcionadas por esta fuente", - "count": "{count} image provided by this source {count} images provided by this source", - "countMore": "Sobre imágenes {count} proporcionadas por esta fuente" - } - }, - "tag": { - "audio": { - "zero": "No hay archivos de audio con la etiqueta seleccionada", - "count": "Archivo de audio {count} con la etiqueta seleccionada {count} archivos de audio con la etiqueta seleccionada", - "countMore": "Sobre archivos de audio {count} con la etiqueta seleccionada" - }, - "image": { - "zero": "No hay imágenes con la etiqueta seleccionada", - "count": "{count} image with the selected tag {count} imágenes con la etiqueta seleccionada", - "countMore": "Sobre {count} imágenes con la etiqueta seleccionada" - } - } - } - } + "flagStatus.nonexistent": "Inexistentes", + "flagStatus.on": "On", + "flagStatus.off": "Fuera.", + "footer.wordpressAffiliation": "Parte del proyecto {wordpress}", + "footer.wip": "🚧", + "language.language": "Idioma", + "recentSearches.heading": "Búsquedas recientes", + "recentSearches.clear.text": "Despejado", + "recentSearches.clear.label": "Búsquedas recientes claras", + "recentSearches.clearSingle.label": "Clear recent search '{entry} '", + "recentSearches.none": "No hay búsquedas recientes para mostrar.", + "recentSearches.disclaimer": "Openverse no almacena sus búsquedas recientes, esta información se mantiene localmente en su navegador.", + "report.imageDetails": "Ver detalles de la imagen", + "sensitiveContent.title.image": "Esta imagen puede contener contenido sensible.", + "sensitiveContent.title.audio": "Esta pista de audio puede contener contenido sensible.", + "sensitiveContent.creator": "Creador", + "sensitiveContent.singleResult.title": "Contenido sensible", + "sensitiveContent.singleResult.hide": "Ocultar contenido", + "sensitiveContent.singleResult.show": "Mostrar contenido", + "sensitiveContent.singleResult.explanation": "Este trabajo está marcado como sensible por las siguientes razones:", + "sensitiveContent.singleResult.learnMore": "{link} sobre cómo {openverse} maneja contenido sensible.", + "sensitiveContent.singleResult.link": "Aprender más", + "sensitiveContent.reasons.providerSuppliedSensitive": "La fuente de este trabajo lo ha marcado como sensible.", + "sensitiveContent.reasons.sensitiveText": "{openverse} ha detectado texto potencialmente sensible.", + "sensitiveContent.reasons.userReportedSensitive": "Los usuarios de {openverse} han reportado este trabajo como sensible.", + "collection.heading.tag": "Tag", + "collection.heading.creator": "Creador", + "collection.heading.source": "Fuente", + "collection.pageTitle.tag.audio": "{tag} audio tracks", + "collection.pageTitle.tag.image": "{tag} images", + "collection.pageTitle.source.audio": "{source} audio tracks", + "collection.pageTitle.source.image": "{source} images", + "collection.link.source": "Sitio de código abierto", + "collection.link.creator": "Página de creador abierta", + "collection.ariaLabel.creator.audio": "Archivos de audio de {creator} en {source}", + "collection.ariaLabel.creator.image": "Images by {creator} in {source}", + "collection.ariaLabel.source.audio": "Archivos de audio de {source}", + "collection.ariaLabel.source.image": "Imágenes de {source}", + "collection.ariaLabel.tag.audio": "Archivos de audio con la etiqueta {tag}", + "collection.ariaLabel.tag.image": "Imágenes con la etiqueta {tag}", + "collection.resultCountLabel.creator.audio.zero": "No hay archivos de audio de este creador.", + "collection.resultCountLabel.creator.audio.count": "{count} audio de este creador.|{count} archivos de audio de este creador.", + "collection.resultCountLabel.creator.audio.countMore": "Sobre archivos de audio {count} de este creador.", + "collection.resultCountLabel.creator.image.zero": "No hay imágenes de este creador.", + "collection.resultCountLabel.creator.image.count": "{count} imágen de este creador|{count} imágens de este creador.", + "collection.resultCountLabel.creator.image.countMore": "Sobre las imágenes de {count} de este creador.", + "collection.resultCountLabel.source.audio.zero": "No hay archivos de audio proporcionados por esta fuente", + "collection.resultCountLabel.source.audio.count": "{count} archivo de audio proporcionado por esta fuente Archivos de audio {count} proporcionados por esta fuente", + "collection.resultCountLabel.source.audio.countMore": "Más archivos de audio {count} proporcionados por esta fuente", + "collection.resultCountLabel.source.image.zero": "No hay imágenes proporcionadas por esta fuente", + "collection.resultCountLabel.source.image.count": "{count} image provided by this source {count} images provided by this source", + "collection.resultCountLabel.source.image.countMore": "Sobre imágenes {count} proporcionadas por esta fuente", + "collection.resultCountLabel.tag.audio.zero": "No hay archivos de audio con la etiqueta seleccionada", + "collection.resultCountLabel.tag.audio.count": "Archivo de audio {count} con la etiqueta seleccionada {count} archivos de audio con la etiqueta seleccionada", + "collection.resultCountLabel.tag.audio.countMore": "Sobre archivos de audio {count} con la etiqueta seleccionada", + "collection.resultCountLabel.tag.image.zero": "No hay imágenes con la etiqueta seleccionada", + "collection.resultCountLabel.tag.image.count": "{count} image with the selected tag {count} imágenes con la etiqueta seleccionada", + "collection.resultCountLabel.tag.image.countMore": "Sobre {count} imágenes con la etiqueta seleccionada" } diff --git a/frontend/test/locales/ru.json b/frontend/test/locales/ru.json index c77de966053..ea694d97508 100644 --- a/frontend/test/locales/ru.json +++ b/frontend/test/locales/ru.json @@ -1,326 +1,152 @@ { - "hero": { - "disclaimer": { - "license": "лицензией Creative Commons" - }, - "search": { - "placeholder": "Поиск содержимого" - }, - "aria": { - "searchType": "Тип поиска", - "search": "поиск" - }, - "licenseFilter": { - "label": "Я хочу что-то, что можно" - } - }, - "audioDetails": { - "information": "Информация об аудио", - "table": { - "genre": "Жанр", - "filetype": "Формат", - "sampleRate": "Частота дискретизации", - "category": "Тип", - "album": "Альбом" - }, - "relatedAudios": "Похожее аудио", - "genreLabel": "Жанр" - }, - "mediaDetails": { - "contentReport": { - "short": "Пожаловаться", - "form": { - "sensitive": { - "subLabel": "Необязательно", - "option": "Содержит конфиденциальный контент", - "placeholder": "По желанию укажите описание" - }, - "other": { - "placeholder": "Введите не менее 20 символов.", - "subLabel": "Обязательно", - "note": "Опишите проблему.", - "option": "Прочее" - }, - "cancel": "Отмена", - "question": "В чём причина?", - "submit": "Пожаловаться" - }, - "success": { - "note": "Спасибо, что сообщили об этом содержимом. Мы рекомендуем сделать то же самое в его источнике, {source}.", - "title": "Жалоба отправлена" - }, - "failure": { - "note": "Что-то пошло не так. Пожалуйста, повторите попытку позже.", - "title": "Жалоба не может быть отправлена" - } - }, - "reuse": { - "tool": { - "content": "Узнайте больше про инструмент {link}" - }, - "copyLicense": { - "title": "Поблагодарить автора", - "html": "HTML" - }, - "image": "Изображение", - "audio": "Звук", - "title": "Повторно используемое содержимое", - "attribution": "Это изображение отмечено как использующее лицензию {link}.", - "toolHeader": "Public Domain", - "licenseHeader": "Лицензия" - }, - "loading": "Загрузка...", - "sourceLabel": "Источник", - "providerLabel": "Поставщик" - }, - "header": { - "filterButton": { - "withCount": "{count} Фильтр|{count} Фильтра|{count} Фильтров" - }, - "aria": { - "search": "поиск", - "srSearch": "кнопка поиска", - "menu": "меню", - "primary": "основное" - }, - "placeholder": "Поиск по всему содержимому", - "notification": { - "okay": "ОК", - "dismiss": "Закрыть" - }, - "aboutTab": "О нас" - }, - "filters": { - "licenses": { - "title": "Лицензии", - "byNcNd": "BY-NC-ND", - "byNcSa": "BY-NC-SA", - "byNd": "BY-ND", - "byNc": "BY-NC", - "bySa": "BY-SA", - "by": "BY", - "pdm": "Public Domain Mark", - "cc0": "CC0" - }, - "aria": { - "removeFilter": "Очистить фильтр {label}" - }, - "licenseTypes": { - "title": "Использование", - "modification": "Изменить или адаптировать", - "commercial": "Использовать в коммерческих целях" - }, - "imageProviders": { - "title": "Источник" - }, - "audioProviders": { - "title": "Источник" - }, - "audioCategories": { - "title": "Категория аудиозаписей", - "podcast": "Подкасты", - "sound": "Звуковые эффекты", - "music": "Музыка" - }, - "imageCategories": { - "title": "Тип изображения", - "illustration": "Иллюстрации", - "photograph": "Фотографии" - }, - "audioExtensions": { - "title": "Тип файла", - "flac": "FLAC", - "ogg": "OGG", - "mp3": "MP3" - }, - "imageExtensions": { - "title": "Тип файла", - "svg": "SVG", - "gif": "GIF", - "png": "PNG", - "jpg": "JPEG" - }, - "aspectRatios": { - "title": "Формат кадра", - "square": "Квадрат", - "wide": "Широкий", - "tall": "Высокий" - }, - "sizes": { - "title": "Размер изображения", - "large": "Большой", - "medium": "Средний", - "small": "Небольшой" - }, - "mature": { - "title": "Настройки поиска", - "enable": "Показывать содержимое для взрослых" - }, - "durations": { - "medium": "Средняя", - "title": "Продолжительность", - "long": "Длинные", - "short": "Короткие" - }, - "creator": { - "title": "Поиск по автору" - }, - "title": "Фильтры", - "filterBy": "Фильтровать по" - }, - "filterList": { - "filterBy": "Фильтр по", - "categoryAria": "Список фильтров для категории {categoryName}", - "show": "Показать результаты", - "clear": "Очистить фильтры", - "hide": "Скрыть фильтры" - }, - "searchType": { - "heading": "Типы содержимого", - "all": "Всё содержимое", - "video": "Видео", - "audio": "Аудио", - "image": "Изображения" - }, - "modal": { - "close": "Закрыть" - }, - "licenseReadableNames": { - "pdm": "Знак общественного достояния" - }, - "search": { - "search": "Поиск" - }, - "sources": { - "title": "Источники", - "providers": { - "source": "Источник" - }, - "issueButton": "Предложить новый источник", - "ccContent": { - "smithsonian": "Смитсоновский институт", - "europeanaApi": "API Europeana" - }, - "singleName": "Источник" - }, - "searchGuide": { - "exact": { - "ariaLabel": "в кавычках, Клод Моне", - "claudeMonet": "\"Клод Моне\"" - } - }, - "browsePage": { - "searchRating": { - "content": "Эти результаты релевантны?", - "feedbackThanks": "Спасибо за ваш отзыв!", - "no": "Нет", - "yes": "Да" - }, - "searchForm": { - "button": "Поиск", - "video": "видео", - "collectionPlaceholder": "Искать в этой коллекции" - }, - "allResultCount": "{localeCount} результат|{localeCount} результата|{localeCount} результатов", - "allNoResults": "Нет результатов", - "aria": { - "relevance": { - "no": "результат релевантен? ответ: нет", - "yes": "результат релевантен? ответ: да" - }, - "removeFilter": "удалить фильтр", - "scroll": "прокрутить наверх" - }, - "fetchingError": "Ошибка при получении {type}:", - "licenseDescription": { - "sa": "Адаптации должны распространяться на тех же условиях.", - "nd": "Адаптация или производные работы не разрешаются.", - "nc": "Только для некоммерческого использования." - }, - "load": "Загрузить ещё результаты", - "allResultCountMore": "Более {localeCount} результата|Более {localeCount} результатов|Более {localeCount} результатов" - }, - "imageDetails": { - "information": { - "source": "Источник", - "provider": "Поставщик", - "pixels": "пикс.", - "dimensions": "Размеры", - "type": "Тип" - }, - "aria": { - "creatorUrl": "Автор {creator}" - }, - "relatedImages": "Связанные изображения" - }, - "externalSources": { - "card": { - "checkboxes": { - "title": "Использование" - } - }, - "form": { - "noResultsTitle": "Не найдено {type} по запросу \"{query}\".", - "caption": "Нажмите на источники ниже для прямого поиска в других коллекциях {type} с лицензиями CC.{break}Использование фильтров не поддерживается в {filter}.", - "supportedTitle": "Не нашли то, что вы искали? Попробуйте дополнительные источники {type}." - } - }, - "downloadButton": { - "download": "Скачать", - "aria": { - "dropdownLabel": "Выберите формат файла для скачивания" - } - }, + "hero.disclaimer.license": "лицензией Creative Commons", + "hero.search.placeholder": "Поиск содержимого", + "audioDetails.table.genre": "Жанр", + "audioDetails.table.filetype": "Формат", + "audioDetails.table.sampleRate": "Частота дискретизации", + "audioDetails.table.album": "Альбом", + "audioDetails.relatedAudios": "Похожее аудио", + "audioDetails.genreLabel": "Жанр", + "mediaDetails.contentReport.short": "Пожаловаться", + "mediaDetails.contentReport.form.sensitive.subLabel": "Необязательно", + "mediaDetails.contentReport.form.sensitive.option": "Содержит конфиденциальный контент", + "mediaDetails.contentReport.form.sensitive.placeholder": "По желанию укажите описание", + "mediaDetails.contentReport.form.other.placeholder": "Введите не менее 20 символов.", + "mediaDetails.contentReport.form.other.subLabel": "Обязательно", + "mediaDetails.contentReport.form.other.note": "Опишите проблему.", + "mediaDetails.contentReport.form.other.option": "Прочее", + "mediaDetails.contentReport.form.cancel": "Отмена", + "mediaDetails.contentReport.form.question": "В чём причина?", + "mediaDetails.contentReport.form.submit": "Пожаловаться", + "mediaDetails.contentReport.success.note": "Спасибо, что сообщили об этом содержимом. Мы рекомендуем сделать то же самое в его источнике, {source}.", + "mediaDetails.contentReport.success.title": "Жалоба отправлена", + "mediaDetails.contentReport.failure.note": "Что-то пошло не так. Пожалуйста, повторите попытку позже.", + "mediaDetails.contentReport.failure.title": "Жалоба не может быть отправлена", + "mediaDetails.reuse.tool.content": "Узнайте больше про инструмент {link}", + "mediaDetails.reuse.copyLicense.title": "Поблагодарить автора", + "mediaDetails.reuse.copyLicense.html": "HTML", + "mediaDetails.reuse.image": "Изображение", + "mediaDetails.reuse.audio": "Звук", + "mediaDetails.reuse.title": "Повторно используемое содержимое", + "mediaDetails.reuse.attribution": "Это изображение отмечено как использующее лицензию {link}.", + "mediaDetails.reuse.toolHeader": "Public Domain", + "mediaDetails.reuse.licenseHeader": "Лицензия", + "mediaDetails.loading": "Загрузка...", + "mediaDetails.sourceLabel": "Источник", + "mediaDetails.providerLabel": "Поставщик", + "header.filterButton.withCount": "{count} Фильтр|{count} Фильтра|{count} Фильтров", + "header.aria.search": "поиск", + "header.aria.srSearch": "кнопка поиска", + "header.aria.menu": "меню", + "header.aria.primary": "основное", + "header.placeholder": "Поиск по всему содержимому", + "header.aboutTab": "О нас", + "filters.licenses.title": "Лицензии", + "filters.licenses.byNcNd": "BY-NC-ND", + "filters.licenses.byNcSa": "BY-NC-SA", + "filters.licenses.byNd": "BY-ND", + "filters.licenses.byNc": "BY-NC", + "filters.licenses.bySa": "BY-SA", + "filters.licenses.by": "BY", + "filters.licenses.pdm": "Public Domain Mark", + "filters.licenses.cc0": "CC0", + "filters.aria.removeFilter": "Очистить фильтр {label}", + "filters.licenseTypes.title": "Использование", + "filters.licenseTypes.modification": "Изменить или адаптировать", + "filters.licenseTypes.commercial": "Использовать в коммерческих целях", + "filters.imageProviders.title": "Источник", + "filters.audioProviders.title": "Источник", + "filters.audioCategories.title": "Категория аудиозаписей", + "filters.audioCategories.podcast": "Подкасты", + "filters.audioCategories.sound": "Звуковые эффекты", + "filters.audioCategories.music": "Музыка", + "filters.imageCategories.title": "Тип изображения", + "filters.imageCategories.illustration": "Иллюстрации", + "filters.imageCategories.photograph": "Фотографии", + "filters.audioExtensions.title": "Тип файла", + "filters.audioExtensions.flac": "FLAC", + "filters.audioExtensions.ogg": "OGG", + "filters.audioExtensions.mp3": "MP3", + "filters.imageExtensions.title": "Тип файла", + "filters.imageExtensions.svg": "SVG", + "filters.imageExtensions.gif": "GIF", + "filters.imageExtensions.png": "PNG", + "filters.imageExtensions.jpg": "JPEG", + "filters.aspectRatios.title": "Формат кадра", + "filters.aspectRatios.square": "Квадрат", + "filters.aspectRatios.wide": "Широкий", + "filters.aspectRatios.tall": "Высокий", + "filters.sizes.title": "Размер изображения", + "filters.sizes.large": "Большой", + "filters.sizes.medium": "Средний", + "filters.sizes.small": "Небольшой", + "filters.lengths.medium": "Средняя", + "filters.lengths.title": "Продолжительность", + "filters.lengths.long": "Длинные", + "filters.lengths.short": "Короткие", + "filters.creator.title": "Поиск по автору", + "filters.title": "Фильтры", + "filters.filterBy": "Фильтровать по", + "filterList.filterBy": "Фильтр по", + "filterList.categoryAria": "Список фильтров для категории {categoryName}", + "filterList.show": "Показать результаты", + "filterList.clear": "Очистить фильтры", + "filterList.hide": "Скрыть фильтры", + "searchType.heading": "Типы содержимого", + "searchType.all": "Всё содержимое", + "searchType.video": "Видео", + "searchType.audio": "Аудио", + "searchType.image": "Изображения", + "modal.close": "Закрыть", + "licenseReadableNames.pdm": "Знак общественного достояния", + "search.search": "Поиск", + "sources.title": "Источники", + "sources.providers.source": "Источник", + "sources.issueButton": "Предложить новый источник", + "sources.singleName": "Источник", + "searchGuide.exact.ariaLabel": "в кавычках, Клод Моне", + "searchGuide.exact.claudeMonet": "\"Клод Моне\"", + "browsePage.searchRating.content": "Эти результаты релевантны?", + "browsePage.searchRating.feedbackThanks": "Спасибо за ваш отзыв!", + "browsePage.searchRating.no": "Нет", + "browsePage.searchRating.yes": "Да", + "browsePage.searchForm.button": "Поиск", + "browsePage.searchForm.video": "видео", + "browsePage.searchForm.collectionPlaceholder": "Искать в этой коллекции", + "browsePage.allResultCount": "{localeCount} результат|{localeCount} результата|{localeCount} результатов", + "browsePage.allNoResults": "Нет результатов", + "browsePage.aria.removeFilter": "удалить фильтр", + "browsePage.aria.scroll": "прокрутить наверх", + "browsePage.fetchingError": "Ошибка при получении {type}:", + "browsePage.licenseDescription.sa": "Адаптации должны распространяться на тех же условиях.", + "browsePage.licenseDescription.nd": "Адаптация или производные работы не разрешаются.", + "browsePage.licenseDescription.nc": "Только для некоммерческого использования.", + "browsePage.load": "Загрузить ещё результаты", + "browsePage.allResultCountMore": "Более {localeCount} результата|Более {localeCount} результатов|Более {localeCount} результатов", + "imageDetails.information.pixels": "пикс.", + "imageDetails.information.dimensions": "Размеры", + "imageDetails.aria.creatorUrl": "Автор {creator}", + "imageDetails.relatedImages": "Связанные изображения", + "externalSources.form.supportedTitle": "Не нашли то, что вы искали? Попробуйте дополнительные источники {type}.", "interpunct": "•", - "error": { - "audio": "аудио", - "image": "изображение", - "occurred": "Произошла ошибка" - }, - "dropdownButton": { - "aria": { - "arrowLabel": "Выпадающий список" - } - }, - "playPause": { - "pause": "Приостановить", - "play": "Воспроизвести" - }, - "audioTrack": { - "messages": { - "err_unsupported": "Этот формат аудио не поддерживается вашим браузером.", - "err_decode": "Невозможно декодировать аудио.", - "err_network": "Произошла ошибка сети.", - "err_aborted": "Вы отменили воспроизведение." - } - }, - "waveform": { - "currentTime": "{time} секунда|{time} секунды|{time} секунд" - }, - "externalSourcesPage": { - "why": { - "feedbackSuggestions": "Мы надеемся, что вам нравится. Если у вас есть предложения по улучшению, то есть {feedback}.", - "feedbackLink": "отзыв", - "ariaLabel": "отзыв" - }, - "new": { - "email": "email" - }, - "use": "Использование" - }, - "browsers": { - "edge": "Edge", - "opera": "Opera", - "firefox": "Firefox", - "chrome": "Chrome" - }, - "singleResult": { - "back": "Назад к результатам поиска" - }, - "about": { - "planning": { - "working": "над чем мы сейчас работаем", - "meta": "Мета поиск" - } - } + "error.audio": "аудио", + "error.image": "изображение", + "error.occurred": "Произошла ошибка", + "playPause.pause": "Приостановить", + "playPause.play": "Воспроизвести", + "audioTrack.messages.err_unsupported": "Этот формат аудио не поддерживается вашим браузером.", + "audioTrack.messages.err_decode": "Невозможно декодировать аудио.", + "audioTrack.messages.err_network": "Произошла ошибка сети.", + "audioTrack.messages.err_aborted": "Вы отменили воспроизведение.", + "waveform.currentTime": "{time} секунда|{time} секунды|{time} секунд", + "externalSourcesPage.why.feedbackSuggestions": "Мы надеемся, что вам нравится. Если у вас есть предложения по улучшению, то есть {feedback}.", + "externalSourcesPage.why.feedbackLink": "отзыв", + "externalSourcesPage.why.ariaLabel": "отзыв", + "externalSourcesPage.new.email": "email", + "browsers.edge": "Edge", + "browsers.opera": "Opera", + "browsers.firefox": "Firefox", + "browsers.chrome": "Chrome", + "singleResult.back": "Назад к результатам поиска", + "about.planning.working": "над чем мы сейчас работаем" } diff --git a/frontend/test/locales/valid-locales.json b/frontend/test/locales/valid-locales.json index 3d14df2336d..37750db8f1a 100644 --- a/frontend/test/locales/valid-locales.json +++ b/frontend/test/locales/valid-locales.json @@ -4,7 +4,6 @@ "name": "Arabic", "nativeName": "العربية", "language": "ar", - "wpLocale": "ar", "dir": "rtl", "translated": 100, "file": "ar.json" @@ -14,7 +13,6 @@ "name": "Spanish (Spain)", "nativeName": "Español", "language": "es", - "wpLocale": "es_ES", "dir": "ltr", "translated": 100, "file": "es.json" @@ -24,7 +22,6 @@ "name": "Russian", "nativeName": "Русский", "language": "ru", - "wpLocale": "ru_RU", "dir": "ltr", "translated": 48, "file": "ru.json" diff --git a/frontend/test/playwright/e2e/filters.spec.ts b/frontend/test/playwright/e2e/filters.spec.ts index 038b86433c1..aa581ae1683 100644 --- a/frontend/test/playwright/e2e/filters.spec.ts +++ b/frontend/test/playwright/e2e/filters.spec.ts @@ -131,9 +131,10 @@ breakpoints.describeMobileAndDesktop(({ breakpoint }) => { // Ignore the "+" licenses which are not presented on the page // `exact: true` is required in locators later in this test to prevent "Attribution" from matching // all CC licenses with the BY element (all of them :P) - const allLicenses = Object.values(enMessages.licenseReadableNames).filter( - (l) => !l.includes("Plus") - ) + const readableLicenseNames = Object.entries(enMessages) + .filter((message) => message[0].startsWith("licenseReadableNames")) + .map((message) => message[1]) + const allLicenses = readableLicenseNames.filter((l) => !l.includes("Plus")) const nonCommercialLicenses = allLicenses.filter((l) => l.includes("NonCommercial") ) diff --git a/frontend/test/playwright/e2e/report-media.spec.ts b/frontend/test/playwright/e2e/report-media.spec.ts index 784aca94911..28ddb8f6a11 100644 --- a/frontend/test/playwright/e2e/report-media.spec.ts +++ b/frontend/test/playwright/e2e/report-media.spec.ts @@ -148,7 +148,7 @@ supportedMediaTypes.forEach((mediaType) => { await page .getByRole("dialog", { - name: t("mediaDetails.contentReport.successTitle"), + name: t("mediaDetails.contentReport.success.title"), }) .isVisible() diff --git a/frontend/test/playwright/utils/i18n.ts b/frontend/test/playwright/utils/i18n.ts index 4c369870cd4..a3271c3c48d 100644 --- a/frontend/test/playwright/utils/i18n.ts +++ b/frontend/test/playwright/utils/i18n.ts @@ -3,29 +3,13 @@ import esMessages from "~~/test/locales/es.json" import ruMessages from "~~/test/locales/ru.json" import enMessages from "~~/i18n/locales/en.json" -const messages: Record> = { +const messages: Record> = { ltr: enMessages, rtl: rtlMessages, es: esMessages, ru: ruMessages, } -const getNestedProperty = ( - obj: Record, - path: string -): string => { - const value = path - .split(".") - .reduce((acc: string | Record, part) => { - if (typeof acc === "string") { - return acc - } - if (Object.keys(acc as Record).includes(part)) { - return (acc as Record>)[part] - } - return "" - }, obj) - return typeof value === "string" ? value : JSON.stringify(value) -} + /** * Simplified i18n t function that returns English messages for `ltr` and Arabic for `rtl`. * It can handle nested labels that use the dot notation ('header.title'). @@ -38,14 +22,19 @@ export const t = ( dir: LanguageDirection = "ltr", locale?: "es" | "ru" ): string => { - let value = "" - if (locale) { - value = getNestedProperty(messages[locale], path) - } else if (dir === "rtl") { - value = getNestedProperty(messages.rtl, path) + const value = locale + ? messages[locale][path] + : messages[dir][path] + ? messages[dir][path] + : messages.ltr[path] + + if (!value) { + throw new Error( + `Missing translation for "${path}" (locale ${locale}, dir ${dir})` + ) } - const result = value === "" ? getNestedProperty(messages.ltr, path) : value - return result.replace("{openverse}", "Openverse") + + return value.replace("{openverse}", "Openverse") } export const languageDirections = ["ltr", "rtl"] as const export type LanguageDirection = (typeof languageDirections)[number] diff --git a/frontend/test/playwright/utils/navigation.ts b/frontend/test/playwright/utils/navigation.ts index f1f21ee8dfc..a5a5879e801 100644 --- a/frontend/test/playwright/utils/navigation.ts +++ b/frontend/test/playwright/utils/navigation.ts @@ -32,7 +32,7 @@ export const searchTypeNames = { [AUDIO]: t("searchType.audio", "ltr"), [IMAGE]: t("searchType.image", "ltr"), [VIDEO]: t("searchType.video", "ltr"), - [MODEL_3D]: t("searchType.model-3d", "ltr"), + [MODEL_3D]: t("searchType.model3d", "ltr"), }, rtl: { [ALL_MEDIA]: t("searchType.all", "rtl"), diff --git a/frontend/test/unit/specs/utils/attribution-html.spec.ts b/frontend/test/unit/specs/utils/attribution-html.spec.ts index eb2a449a902..efc242f545e 100644 --- a/frontend/test/unit/specs/utils/attribution-html.spec.ts +++ b/frontend/test/unit/specs/utils/attribution-html.spec.ts @@ -28,13 +28,12 @@ describe("getAttribution", () => { expect(attributionP.textContent?.trim()).toEqual(attributionText) }) - // TODO: fix fakeT function it("returns attribution for media without i18n", async () => { - // const attributionText = '"Title" by Creator is marked with PDM 1.0 .' + const attributionText = '"Title" by Creator is marked with PDM 1.0 .' console.log(getAttribution(mediaItem, null)) document.body.innerHTML = getAttribution(mediaItem, null) const attributionP = document.getElementsByClassName("attribution")[0] - expect(attributionP.textContent?.trim()).toBe("") + expect(attributionP.textContent?.trim()).toBe(attributionText) }) it("uses generic title if not known", async () => { diff --git a/justfile b/justfile index 2870c41bfd8..a1b43276426 100644 --- a/justfile +++ b/justfile @@ -57,7 +57,7 @@ locales mode="test": if [ "{{ mode }}" = "production" ]; then just frontend/run i18n elif [ "{{ mode }}" = "test" ]; then - just frontend/run i18n:copy-test-locales + just frontend/run i18n:test else echo "Invalid mode {{ mode }}. Using only the `en` locale. To set up more locales, use 'production' or 'test'." fi diff --git a/packages/js/eslint-plugin/src/configs/vue.ts b/packages/js/eslint-plugin/src/configs/vue.ts index dabbdf451f1..17f848a45d8 100644 --- a/packages/js/eslint-plugin/src/configs/vue.ts +++ b/packages/js/eslint-plugin/src/configs/vue.ts @@ -18,7 +18,7 @@ export default tseslint.config( files: ["*.vue", "**/*.vue"], settings: { "vue-i18n": { - localeDir: "./frontend/i18n/locales/*.{json}", + localeDir: "./frontend/i18n/locales/*.json", messageSyntaxVersion: "^9.0.0", }, }, @@ -158,13 +158,24 @@ export default tseslint.config( files: ["frontend/i18n/data/en.json5", "frontend/test/locales/*.json"], rules: { "@openverse/translation-strings": ["error"], - "jsonc/key-name-casing": [ + "@openverse/key-name-casing": [ "error", { - camelCase: true, - "kebab-case": false, - snake_case: true, // for err_* keys - ignores: ["ncSampling+", "sampling+"], + camelCaseWithDot: true, + // keys that still have snake_case_with_dot are in `ignores` to prevent accidentally adding more such mixed keys + snake_case_with_dot: false, + ignores: [ + "ncSampling+", + "sampling+", + "sound_effect", + "digitized_artwork", + "err_network", + "err_decode", + "err_unallowed", + "err_unknown", + "err_unsupported", + "err_aborted", + ], }, ], }, diff --git a/packages/js/eslint-plugin/src/index.ts b/packages/js/eslint-plugin/src/index.ts index a46d92dc37c..39599ec0a12 100644 --- a/packages/js/eslint-plugin/src/index.ts +++ b/packages/js/eslint-plugin/src/index.ts @@ -20,6 +20,7 @@ const plugin = { "analytics-configuration": rules["analytics-configuration"], "no-unexplained-disabled-test": rules["no-unexplained-disabled-test"], "translation-strings": rules["translation-strings"], + "key-name-casing": rules["key-name-casing"], }, } diff --git a/packages/js/eslint-plugin/src/rules/index.ts b/packages/js/eslint-plugin/src/rules/index.ts index 49499d9e8a0..1dab80f184c 100644 --- a/packages/js/eslint-plugin/src/rules/index.ts +++ b/packages/js/eslint-plugin/src/rules/index.ts @@ -1,9 +1,11 @@ import { analyticsConfiguration } from "./analytics-configuration" import { noUnexplainedDisabledTest } from "./no-unexplained-disabled-test" import { translationStrings } from "./translation-strings" +import { keyNameCasing } from "./key-name-casing" export default { "analytics-configuration": analyticsConfiguration, "no-unexplained-disabled-test": noUnexplainedDisabledTest, "translation-strings": translationStrings, + "key-name-casing": keyNameCasing, } as const diff --git a/packages/js/eslint-plugin/src/rules/key-name-casing.ts b/packages/js/eslint-plugin/src/rules/key-name-casing.ts new file mode 100644 index 00000000000..fde216b0110 --- /dev/null +++ b/packages/js/eslint-plugin/src/rules/key-name-casing.ts @@ -0,0 +1,110 @@ +import { SourceCode } from "eslint" +import { JSONProperty } from "jsonc-eslint-parser/lib/parser/ast" + +import { OpenverseRule } from "../utils/rule-creator" + +type CaseFormat = "camelCaseWithDot" | "snake_case_with_dot" + +/** + * Validates if a string matches the specified case format + */ +function isValidCase(str: string, format: CaseFormat): boolean { + const patterns = { + camelCaseWithDot: /^[a-z][a-zA-Z0-9]*$/, + snake_case_with_dot: /^[a-z0-9]+(_[a-z0-9]+)*$/, + } + + if (str.includes(".")) { + return str.split(".").every((part) => { + if (/^\d+$/.test(part)) { + return true + } + return patterns[format].test(part) + }) + } + return patterns[format].test(str) +} + +type RuleOptions = { + camelCaseWithDot?: boolean + snake_case_with_dot?: boolean + ignores?: string[] +} + +export const keyNameCasing = OpenverseRule< + [RuleOptions], + "incorrectKeyNameComment" +>({ + name: "key-name-casing", + meta: { + docs: { + description: "enforce naming convention to property key names", + }, + schema: [ + { + type: "object", + properties: { + camelCaseWithDot: { type: "boolean", default: true }, + snake_case_with_dot: { type: "boolean", default: true }, + ignores: { + type: "array", + items: { type: "string" }, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], + messages: { + incorrectKeyNameComment: + "Property name `{{name}}` must match one of the following formats: {{formats}}", + }, + type: "suggestion", + }, + defaultOptions: [ + { + camelCaseWithDot: true, + snake_case_with_dot: true, + }, + ], + create(context) { + const sourceCode = context.sourceCode + if (!(sourceCode.parserServices as SourceCode.ParserServices).isJSON) { + return {} + } + + const options = { ...context.options[0] } + const ignores = options.ignores?.map((pattern) => new RegExp(pattern)) || [] + const enabledFormats = ( + ["camelCaseWithDot", "snake_case_with_dot"] as const + ).filter((format) => options[format] !== false) + + function isValidName(name: string): boolean { + if (!enabledFormats.length || ignores.some((regex) => regex.test(name))) { + return true + } + + return enabledFormats.some((format) => isValidCase(name, format)) + } + + return { + JSONProperty(node: JSONProperty) { + const name = + node.key.type === "JSONLiteral" && typeof node.key.value === "string" + ? node.key.value + : sourceCode.text.slice(...node.key.range) + + if (!isValidName(name)) { + context.report({ + loc: node.key.loc, + messageId: "incorrectKeyNameComment", + data: { + name, + formats: enabledFormats.join(", "), + }, + }) + } + }, + } + }, +}) diff --git a/packages/js/eslint-plugin/test/rules/key-name-casing.spec.ts b/packages/js/eslint-plugin/test/rules/key-name-casing.spec.ts new file mode 100644 index 00000000000..add538dcb44 --- /dev/null +++ b/packages/js/eslint-plugin/test/rules/key-name-casing.spec.ts @@ -0,0 +1,137 @@ +import { RuleTester } from "@typescript-eslint/rule-tester" +import jsoncParser from "jsonc-eslint-parser" + +import { keyNameCasing } from "../../src/rules/key-name-casing" + +const tester = new RuleTester() + +const baseTestCase = { + filename: "en.json5", + languageOptions: { parser: jsoncParser }, +} + +const invalidCamelCaseKeys = [ + "PascalCase", + "kebab-case", + "snake_case", + "space case", + "mixed.PascalCase", + "mixed.kebab-case", + "mixed.snake_case", + "mixed.space case", +] + +const invalidSnakeCaseKeys = [ + "camelCase", + "PascalCase", + "kebab-case", + "space case", + "mixed.camelCase", + "mixed.PascalCase", + "mixed.kebab-case", + "mixed.space case", +] + +const validCamelCaseKeys = [ + "camelCase", + "simple", + "nested.camelCase", + "deeply.nested.camelCase", +] + +const validSnakeCaseKeys = [ + "snake_case", + "simple", + "nested.snake_case", + "deeply.nested.snake_case", +] + +tester.run("key-name-casing", keyNameCasing, { + invalid: [ + // Test camelCase violations + ...invalidCamelCaseKeys.map( + (key) => + ({ + ...baseTestCase, + name: `Disallow non-camelCase keys: ${key}`, + code: `{ + "${key}": "value" + }`, + options: [{ camelCaseWithDot: true, snake_case_with_dot: false }], + errors: [ + { + messageId: "incorrectKeyNameComment", + data: { + name: key, + formats: "camelCaseWithDot", + }, + }, + ], + }) as const + ), + // Test snake_case violations + ...invalidSnakeCaseKeys.map( + (key) => + ({ + ...baseTestCase, + name: `Disallow non-snake_case keys: ${key}`, + code: `{ + "${key}": "value" + }`, + options: [{ camelCaseWithDot: false, snake_case_with_dot: true }], + errors: [ + { + messageId: "incorrectKeyNameComment", + data: { + name: key, + formats: "snake_case_with_dot", + }, + }, + ], + }) as const + ), + ], + valid: [ + // Test valid camelCase + ...validCamelCaseKeys.map( + (key) => + ({ + ...baseTestCase, + name: `Allow camelCase keys: ${key}`, + code: `{ + "${key}": "value" + }`, + options: [{ camelCaseWithDot: true, snake_case_with_dot: false }], + }) as const + ), + // Test valid snake_case + ...validSnakeCaseKeys.map( + (key) => + ({ + ...baseTestCase, + name: `Allow snake_case keys: ${key}`, + code: `{ + "${key}": "value" + }`, + options: [{ camelCaseWithDot: false, snake_case_with_dot: true }], + }) as const + ), + // Test ignored patterns + { + ...baseTestCase, + name: "Allow ignored patterns anywhere in the key", + code: `{ + "IGNORED_PATTERN": "value", + "another.IGNORED_PATTERN": "value", + "IGNORED_PATTERN.something": "value" + }`, + options: [ + { + camelCaseWithDot: true, + snake_case_with_dot: true, + ignores: ["IGNORED_PATTERN"], + }, + ], + }, + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4100dbbfffd..9d6b7651a18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,9 @@ importers: jsdom: specifier: ^25.0.0 version: 25.0.1 + json5: + specifier: ^2.2.3 + version: 2.2.3 node-html-parser: specifier: ^6.1.13 version: 6.1.13 diff --git a/utilities/generate_test_locales/__main__.py b/utilities/generate_test_locales/__main__.py index 217a2671dba..810f6572e6c 100644 --- a/utilities/generate_test_locales/__main__.py +++ b/utilities/generate_test_locales/__main__.py @@ -11,7 +11,7 @@ from_lang = "en" # Must be generated using `ov just p frontend i18n:en` -from_json_path = reporoot / "frontend" / "src" / "locales" / "en.json" +from_json_path = reporoot / "frontend" / "i18n" / "locales" / "en.json" to_langs = ["ar", "es"] to_json_paths = {