@hoardodile/i18n 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +81 -0
  3. package/dist/catalogs/ui.d.ts +2 -0
  4. package/dist/catalogs/ui.js +449 -0
  5. package/dist/catalogs/ui.js.map +1 -0
  6. package/dist/catalogs/workbench.d.ts +291 -0
  7. package/dist/catalogs/workbench.js +250 -0
  8. package/dist/catalogs/workbench.js.map +1 -0
  9. package/dist/catalogs-BF4px4Mm.d.ts +12986 -0
  10. package/dist/catalogs.d.ts +2 -0
  11. package/dist/catalogs.js +10794 -0
  12. package/dist/catalogs.js.map +1 -0
  13. package/dist/core.d.ts +32 -0
  14. package/dist/core.js +14 -0
  15. package/dist/core.js.map +1 -0
  16. package/dist/create-i18n.d.ts +35 -0
  17. package/dist/create-i18n.js +30 -0
  18. package/dist/create-i18n.js.map +1 -0
  19. package/dist/index.d.ts +42 -0
  20. package/dist/index.js +11284 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/react.d.ts +1 -0
  23. package/dist/react.js +3 -0
  24. package/dist/react.js.map +1 -0
  25. package/dist/ui-CJPMCZT7.d.ts +548 -0
  26. package/package.json +81 -0
  27. package/src/catalogs/de.json +2155 -0
  28. package/src/catalogs/en.json +2155 -0
  29. package/src/catalogs/es.json +2155 -0
  30. package/src/catalogs/ja.json +2155 -0
  31. package/src/catalogs/ui.ts +30 -0
  32. package/src/catalogs/workbench.ts +29 -0
  33. package/src/catalogs/zh.json +2155 -0
  34. package/src/catalogs.ts +31 -0
  35. package/src/core.ts +40 -0
  36. package/src/create-i18n.ts +52 -0
  37. package/src/index.ts +66 -0
  38. package/src/language.test.ts +70 -0
  39. package/src/parity.test.ts +175 -0
  40. package/src/react.ts +25 -0
  41. package/src/ui/de.json +86 -0
  42. package/src/ui/en.json +86 -0
  43. package/src/ui/es.json +86 -0
  44. package/src/ui/ja.json +86 -0
  45. package/src/ui/zh.json +86 -0
  46. package/src/workbench/de.json +45 -0
  47. package/src/workbench/en.json +45 -0
  48. package/src/workbench/es.json +45 -0
  49. package/src/workbench/ja.json +45 -0
  50. package/src/workbench/zh.json +45 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The app catalog registry: every shipped language's complete JSON
3
+ * catalog plus the single resolution point the Electron shell (main
4
+ * process, wizard, error pages) uses to pick translations.
5
+ *
6
+ * Split from `./core.ts` (pure helpers, no catalogs) so the sandboxed
7
+ * preload and any other bundle that only needs `isSupportedLanguage` /
8
+ * `resolveSystemLanguage` never loads the catalogs.
9
+ *
10
+ * `Record<SupportedLanguage, typeof en>` makes any key drift between
11
+ * catalogs a **compile error** (the five JSON modules are structurally
12
+ * typed); `parity.test.ts` enforces the rest (placeholders, plural
13
+ * pairs, markup tags, ellipsis).
14
+ */
15
+
16
+ import de from "./catalogs/de.json"
17
+ import en from "./catalogs/en.json"
18
+ import es from "./catalogs/es.json"
19
+ import ja from "./catalogs/ja.json"
20
+ import zh from "./catalogs/zh.json"
21
+ import type { SupportedLanguage } from "./core.ts"
22
+
23
+ export const CATALOGS = { en, zh, ja, de, es } as const satisfies Record<
24
+ SupportedLanguage,
25
+ typeof en
26
+ >
27
+
28
+ /** Resolve the app catalog for the active language; pre-SPA (undefined) → English. */
29
+ export function catalogFor(language: SupportedLanguage | undefined): typeof en {
30
+ return language === undefined ? en : CATALOGS[language]
31
+ }
package/src/core.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Locale plumbing shared by the web SPA, the Electron shell and the plugin
3
+ * SDK: the supported-language set and pure helpers. **This module must
4
+ * stay free of catalog imports** — the sandboxed preload imports
5
+ * `isSupportedLanguage` from here, and the catalogs (which weigh far
6
+ * more than this file) live in `./catalogs.ts` (`CATALOGS`,
7
+ * `catalogFor`), `./catalogs/ui.ts` (`UI_CATALOGS`, `uiCatalogFor`) plus
8
+ * the per-language JSON modules.
9
+ *
10
+ * `catalogs/*.json` (the app `translation` namespace) and `ui/*.json`
11
+ * (the shared `ui` namespace consumed by `@hoardodile/ui` and the plugin
12
+ * SDK iframes) must stay in lockstep internally: identical flat key
13
+ * sets, matching interpolation placeholders, and complete `_one`/`_other`
14
+ * pairs. `src/parity.test.ts` enforces this — run it after touching any
15
+ * catalog. `CATALOGS: Record<SupportedLanguage, typeof en>` and
16
+ * `UI_CATALOGS: Record<SupportedLanguage, typeof uiEn>` additionally
17
+ * make any key drift a compile error in `tsc`.
18
+ */
19
+ export const SUPPORTED_LANGUAGES = ["en", "zh", "ja", "de", "es"] as const
20
+ export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]
21
+
22
+ export function isSupportedLanguage(value: string): value is SupportedLanguage {
23
+ return (SUPPORTED_LANGUAGES as readonly string[]).includes(value)
24
+ }
25
+
26
+ /**
27
+ * Map a BCP-47 locale string (e.g. `navigator.language`,
28
+ * `app.getLocale()`, a stored pref) onto the supported set, taking the
29
+ * base code: `"ja-JP"` → `"ja"`, `"de-DE"` → `"de"`, `"es-MX"` → `"es"`,
30
+ * `"zh-CN"`/`"zh-TW"` → `"zh"` (any Chinese base maps to the `zh`
31
+ * catalog, preserving historical behavior). Unknown or missing values
32
+ * fall back to `"en"`.
33
+ */
34
+ export function resolveSystemLanguage(
35
+ raw: string | undefined,
36
+ ): SupportedLanguage {
37
+ const base = raw?.toLowerCase().split("-")[0]
38
+ if (base !== undefined && isSupportedLanguage(base)) return base
39
+ return "en"
40
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The i18next instance factory every React root uses: the web SPA, the
3
+ * desktop wizard/shell pages, the workbench and the plugin SDK iframes
4
+ * all boot their instance with the same options so catalog behavior
5
+ * (fallback, plurals, interpolation, type safety) never diverges between
6
+ * surfaces.
7
+ *
8
+ * This module is deliberately free of catalog imports — plugin iframe
9
+ * bundles import the factory through this subpath and ship only the
10
+ * small `ui`/`plugin` namespaces (the full app catalog would otherwise
11
+ * be pulled into every bundle). Host surfaces use `createI18n` from
12
+ * `@hoardodile/i18n`, which wraps this factory with the shared catalogs.
13
+ *
14
+ * React-free on purpose: the Electron main process and the sandboxed
15
+ * preload keep using `catalogFor` / `uiCatalogFor` directly, and each
16
+ * React root adds its own `react-i18next` binding (e.g.
17
+ * `setI18n(instance)`).
18
+ */
19
+
20
+ import i18next, { type i18n as I18nInstance, type InitOptions } from "i18next"
21
+ import { SUPPORTED_LANGUAGES } from "./core.ts"
22
+
23
+ export type CreateI18nOptions = Omit<InitOptions, "lng" | "resources"> & {
24
+ /** Initial language; undefined lets i18next resolve its default (fallback → "en"). */
25
+ readonly lng?: string
26
+ /** Resource map — the shared catalogs (or the smaller ui/plugin set). */
27
+ readonly resources: InitOptions["resources"]
28
+ }
29
+
30
+ /**
31
+ * Create an i18next instance with the canonical option set. Callers
32
+ * supply the resources explicitly (hosts via the `@hoardodile/i18n`
33
+ * wrapper, plugins via the SDK).
34
+ */
35
+ export function createI18n(options: CreateI18nOptions): I18nInstance {
36
+ const { lng, resources, ...rest } = options
37
+ const instance = i18next.createInstance()
38
+ void instance.init({
39
+ resources,
40
+ lng,
41
+ fallbackLng: "en",
42
+ supportedLngs: [...SUPPORTED_LANGUAGES],
43
+ nonExplicitSupportedLngs: true,
44
+ interpolation: { escapeValue: false },
45
+ returnNull: false,
46
+ returnEmptyString: false,
47
+ compatibilityJSON: "v4",
48
+ pluralSeparator: "_",
49
+ ...rest,
50
+ } satisfies InitOptions)
51
+ return instance
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type en from "./catalogs/en.json"
2
+ import type uiEn from "./ui/en.json"
3
+
4
+ /**
5
+ * Type the `t()` keys of every consumer against the catalogs: a wrong key
6
+ * (typo or a key added to one catalog but not the others) becomes a
7
+ * compile error instead of a silently rendered key name. Declared once
8
+ * here so the web SPA, the desktop surfaces, the workbench and the plugin
9
+ * SDK all resolve the same resources. The `plugin` namespace is typed
10
+ * loosely on purpose — its strings are authored per plugin (their
11
+ * bundles vary), and `@hoardodile/sdk-react`'s `useTranslation` wrapper
12
+ * returns that loose signature to plugin code.
13
+ */
14
+ declare module "i18next" {
15
+ interface CustomTypeOptions {
16
+ defaultNS: "translation"
17
+ resources: {
18
+ translation: typeof en
19
+ ui: typeof uiEn
20
+ plugin: Record<string, string>
21
+ }
22
+ returnNull: false
23
+ returnEmptyString: false
24
+ }
25
+ }
26
+
27
+ import type { i18n as I18nInstance } from "i18next"
28
+ import { UI_CATALOGS } from "./catalogs/ui.ts"
29
+ import { CATALOGS } from "./catalogs.ts"
30
+ import { SUPPORTED_LANGUAGES } from "./core.ts"
31
+ import {
32
+ type CreateI18nOptions,
33
+ createI18n as createI18nCore,
34
+ } from "./create-i18n.ts"
35
+
36
+ /**
37
+ * Host-surface factory: an i18next instance preloaded with the shared
38
+ * catalogs (`translation` + `ui`, all five languages). React roots boot
39
+ * it with the same options everywhere. Plugin iframes use the `createI18n`
40
+ * factory from `@hoardodile/i18n/create-i18n` instead with a resource
41
+ * subset (ui + plugin namespaces) so the full app catalog never enters
42
+ * their bundles.
43
+ */
44
+ export function createI18n(
45
+ options?: Omit<CreateI18nOptions, "resources">,
46
+ ): I18nInstance {
47
+ return createI18nCore({
48
+ resources: Object.fromEntries(
49
+ SUPPORTED_LANGUAGES.map((language) => [
50
+ language,
51
+ { translation: CATALOGS[language], ui: UI_CATALOGS[language] },
52
+ ]),
53
+ ),
54
+ ...options,
55
+ })
56
+ }
57
+
58
+ export { UI_CATALOGS, uiCatalogFor } from "./catalogs/ui.ts"
59
+ export { CATALOGS, catalogFor } from "./catalogs.ts"
60
+ export {
61
+ isSupportedLanguage,
62
+ resolveSystemLanguage,
63
+ SUPPORTED_LANGUAGES,
64
+ type SupportedLanguage,
65
+ } from "./core.ts"
66
+ export type { CreateI18nOptions } from "./create-i18n.ts"
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { describe, expect, it } from "vitest"
5
+ import { UI_CATALOGS, uiCatalogFor } from "./catalogs/ui.ts"
6
+ import { CATALOGS, catalogFor } from "./catalogs.ts"
7
+ import {
8
+ isSupportedLanguage,
9
+ resolveSystemLanguage,
10
+ SUPPORTED_LANGUAGES,
11
+ } from "./core.ts"
12
+
13
+ describe("resolveSystemLanguage", () => {
14
+ it("maps base codes onto the supported set", () => {
15
+ expect(resolveSystemLanguage("ja-JP")).toBe("ja")
16
+ expect(resolveSystemLanguage("de-DE")).toBe("de")
17
+ expect(resolveSystemLanguage("es-MX")).toBe("es")
18
+ expect(resolveSystemLanguage("es")).toBe("es")
19
+ // Any Chinese base maps to the `zh` catalog, preserving history.
20
+ expect(resolveSystemLanguage("zh-CN")).toBe("zh")
21
+ expect(resolveSystemLanguage("zh-TW")).toBe("zh")
22
+ expect(resolveSystemLanguage("ZH-hant")).toBe("zh")
23
+ expect(resolveSystemLanguage("en-US")).toBe("en")
24
+ })
25
+
26
+ it("falls back to English for unknown or missing values", () => {
27
+ expect(resolveSystemLanguage("fr")).toBe("en")
28
+ expect(resolveSystemLanguage("")).toBe("en")
29
+ expect(resolveSystemLanguage(undefined)).toBe("en")
30
+ })
31
+ })
32
+
33
+ describe("catalogFor", () => {
34
+ it("returns the matching catalog for every supported language", () => {
35
+ for (const code of SUPPORTED_LANGUAGES) {
36
+ expect(catalogFor(code), `catalogFor(${code})`).toBe(
37
+ CATALOGS[code as (typeof SUPPORTED_LANGUAGES)[number]],
38
+ )
39
+ }
40
+ })
41
+
42
+ it("falls back to English when no language is pushed yet", () => {
43
+ expect(catalogFor(undefined)).toBe(CATALOGS.en)
44
+ })
45
+ })
46
+
47
+ describe("uiCatalogFor", () => {
48
+ it("returns the matching ui catalog for every supported language", () => {
49
+ for (const code of SUPPORTED_LANGUAGES) {
50
+ expect(uiCatalogFor(code), `uiCatalogFor(${code})`).toBe(
51
+ UI_CATALOGS[code as (typeof SUPPORTED_LANGUAGES)[number]],
52
+ )
53
+ }
54
+ })
55
+
56
+ it("falls back to English when no language is pushed yet", () => {
57
+ expect(uiCatalogFor(undefined)).toBe(UI_CATALOGS.en)
58
+ })
59
+ })
60
+
61
+ describe("isSupportedLanguage", () => {
62
+ it("accepts exactly the supported base codes", () => {
63
+ for (const code of SUPPORTED_LANGUAGES) {
64
+ expect(isSupportedLanguage(code)).toBe(true)
65
+ }
66
+ expect(isSupportedLanguage("de-DE")).toBe(false)
67
+ expect(isSupportedLanguage("fr")).toBe(false)
68
+ expect(isSupportedLanguage("")).toBe(false)
69
+ })
70
+ })
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @vitest-environment node
3
+ *
4
+ * Guardrails that keep the i18n catalogs in lockstep — the app
5
+ * `translation` namespace (`src/catalogs/*.json`), the shared `ui`
6
+ * namespace (`src/ui/*.json`) and the workbench namespace
7
+ * (`src/workbench/*.json`). These caught real regressions
8
+ * historically (dead `_plural` keys under i18next v4, a missing zh
9
+ * mirror, and the desktop shell's hardcoded ternaries), so they are
10
+ * intentionally strict: a new key must be registered in every language
11
+ * before it can ship.
12
+ *
13
+ * Structural rules only (key parity, placeholders, plural pairs, markup
14
+ * tags, ellipsis). Key *naming* is intentionally not enforced — see the
15
+ * documented conventions in `src/core.ts` and the catalog registries.
16
+ */
17
+ import { describe, expect, it } from "vitest"
18
+ import { UI_CATALOGS } from "./catalogs/ui.ts"
19
+ import { WORKBENCH_CATALOGS } from "./catalogs/workbench.ts"
20
+ import { CATALOGS } from "./catalogs.ts"
21
+
22
+ type FlatEntry = { key: string; value: string }
23
+
24
+ function flatten(
25
+ obj: Record<string, unknown>,
26
+ path: string[] = [],
27
+ out: FlatEntry[] = [],
28
+ ): FlatEntry[] {
29
+ for (const [k, v] of Object.entries(obj)) {
30
+ if (typeof v === "object" && v !== null) {
31
+ flatten(v as Record<string, unknown>, [...path, k], out)
32
+ } else {
33
+ out.push({ key: [...path, k].join("."), value: String(v) })
34
+ }
35
+ }
36
+ return out
37
+ }
38
+
39
+ function vars(value: string): string {
40
+ return (value.match(/\{\{[^}]+\}\}/g) ?? [])
41
+ .map((m) => m.slice(2, -2))
42
+ .sort()
43
+ .join(",")
44
+ }
45
+
46
+ /** Markup tags (`<name>`, `</name>`, …) — translations must keep them. */
47
+ function tags(value: string): string {
48
+ return (value.match(/<\/?[a-zA-Z][a-zA-Z0-9-]*>/g) ?? []).sort().join(",")
49
+ }
50
+
51
+ /** Keys that legitimately keep `{{count}}` without a plural pair. */
52
+ const NO_PLURAL_PAIR_ALLOWLIST = new Set([
53
+ "search.sectionCount", // "({{count}})" parenthetical
54
+ "search.viewAll", // "View all ({{count}})"
55
+ "me.custom.unusedCount", // "{{count}} unused" (adjective)
56
+ "me.desktop.lan.moreAddresses", // "Other addresses ({{count}})" parenthetical
57
+ "me.trash.view", // "Review trash ({{count}})"
58
+ "plugins.countInstalled", // "{{count}} installed" (adjective)
59
+ "usage.leaderboard.associatedSessionsShort", // "{{count}} associated" (adjective)
60
+ "categories.panel.dependencyResources", // "{{count}} res" (abbreviation)
61
+ "categories.panel.dependencyCharacters", // "{{count}} char" (abbreviation)
62
+ "categories.panel.tagCharacterCount", // "char {{count}}"
63
+ "categories.panel.tagResourceCount", // "res {{count}}"
64
+ "deleteEntity.usageMessage", // usage noun is passed in (singular when count=1)
65
+ "documents.statusBar.charCount", // "{{count}} / {{max}} chars" (range)
66
+ "sync.banner.overdueDescription", // "{{count}}-day reminder" (compound)
67
+ "trace.overview.moreThanPrev", // "{{count}} more than the previous period"
68
+ "trace.overview.lessThanPrev", // "{{count}} less than the previous period"
69
+ "characters.bulk.toolbarCount", // "{{count}} selected" (adjective)
70
+ "resources.bulk.toolbarCount", // "{{count}} selected" (adjective)
71
+ "characters.bulk.toastAllFailed", // "({{count}} failed)" parenthetical
72
+ "resources.bulk.toastAllFailed", // "({{count}} failed)" parenthetical
73
+ "characters.selectorDialog.confirmCount", // "Confirm ({{count}})"
74
+ "messages.viewReplies", // "View replies ({{count}})"
75
+ ])
76
+
77
+ /** Keys that keep ASCII `...` in the value (URL placeholders). */
78
+ const ASCII_ELLIPSIS_ALLOWLIST = new Set([
79
+ "resources.new.sourceUrlPlaceholder",
80
+ "resources.editDialog.sourceUrlPlaceholder",
81
+ ])
82
+
83
+ function checkCatalogLockstep(
84
+ catalogs: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
85
+ ): void {
86
+ const EN = catalogs.en as Record<string, unknown>
87
+ const enFlat = flatten(EN)
88
+ const enKeys = new Set(enFlat.map((r) => r.key))
89
+ const enByKey = new Map(enFlat.map((r) => [r.key, r.value]))
90
+
91
+ for (const [name, catalog] of Object.entries(catalogs)) {
92
+ const flat = flatten(catalog as unknown as Record<string, unknown>)
93
+ const keys = new Set(flat.map((r) => r.key))
94
+ const byKey = new Map(flat.map((r) => [r.key, r.value]))
95
+
96
+ it(`${name} has identical flat key sets`, () => {
97
+ expect(
98
+ [...enKeys].filter((k) => !keys.has(k)),
99
+ `keys only in en (missing from ${name})`,
100
+ ).toEqual([])
101
+ expect(
102
+ [...keys].filter((k) => !enKeys.has(k)),
103
+ `keys only in ${name}`,
104
+ ).toEqual([])
105
+ })
106
+
107
+ it(`${name} uses the same interpolation placeholders per key`, () => {
108
+ const mismatched: string[] = []
109
+ for (const key of enKeys) {
110
+ const value = byKey.get(key)
111
+ if (
112
+ value !== undefined &&
113
+ vars(enByKey.get(key) ?? "") !== vars(value)
114
+ ) {
115
+ mismatched.push(key)
116
+ }
117
+ }
118
+ expect(mismatched, "placeholder mismatch").toEqual([])
119
+ })
120
+
121
+ it(`${name} keeps <name> markup tags identical per key`, () => {
122
+ const mismatched: string[] = []
123
+ for (const key of enKeys) {
124
+ const value = byKey.get(key)
125
+ if (
126
+ value !== undefined &&
127
+ tags(enByKey.get(key) ?? "") !== tags(value)
128
+ ) {
129
+ mismatched.push(key)
130
+ }
131
+ }
132
+ expect(mismatched, "markup tag mismatch").toEqual([])
133
+ })
134
+
135
+ it(`${name} has complete plural pairs and no legacy suffixes`, () => {
136
+ const bad: string[] = []
137
+ for (const key of keys) {
138
+ if (/_(plural|singular)$/.test(key)) bad.push(`${key} (legacy suffix)`)
139
+ }
140
+ for (const key of keys) {
141
+ const base = key.replace(/_(one|other|few|many|zero)$/, "")
142
+ if (base === key) continue
143
+ if (!keys.has(`${base}_one`) || !keys.has(`${base}_other`)) {
144
+ bad.push(`${key} (incomplete pair)`)
145
+ }
146
+ }
147
+ expect(bad, "plural violations").toEqual([])
148
+ })
149
+
150
+ it(`${name} uses U+2026 ellipsis everywhere except URL placeholders`, () => {
151
+ const violating = flat
152
+ .filter((r) => r.value.includes("..."))
153
+ .map((r) => r.key)
154
+ .filter((k) => !ASCII_ELLIPSIS_ALLOWLIST.has(k))
155
+ expect(violating, "ASCII ellipsis outside allowlist").toEqual([])
156
+ })
157
+ }
158
+
159
+ it("never leaves a count key without a plural pair unless allowlisted", () => {
160
+ const violating: string[] = []
161
+ for (const [key, value] of enByKey) {
162
+ if (!value.includes("{{count}}")) continue
163
+ if (/_(one|other|few|many|zero)$/.test(key)) continue
164
+ if (NO_PLURAL_PAIR_ALLOWLIST.has(key)) continue
165
+ violating.push(key)
166
+ }
167
+ expect(violating, "count key without plural pair").toEqual([])
168
+ })
169
+ }
170
+
171
+ describe("i18n catalog parity", () => {
172
+ checkCatalogLockstep(CATALOGS)
173
+ checkCatalogLockstep(UI_CATALOGS)
174
+ checkCatalogLockstep(WORKBENCH_CATALOGS)
175
+ })
package/src/react.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { I18nextProvider, setI18n } from "react-i18next"
2
+
3
+ /**
4
+ * React binding shared by every host surface. Import the provider and
5
+ * `setI18n` from HERE (not from `react-i18next`) so the provider and
6
+ * `@hoardodile/ui` components resolve the SAME react-i18next context
7
+ * instance: the workspace pins two typescript toolchains and pnpm
8
+ * therefore keeps two physical copies of react-i18next (keyed by peer
9
+ * context), so react-i18next's module-global default instance must NOT
10
+ * be relied on across package boundaries — hosts pass the instance
11
+ * explicitly.
12
+ *
13
+ * The `@hoardodile/ui` components call `useTranslation("ui")` against
14
+ * this context; host roots wrap their tree with the provider:
15
+ *
16
+ * ```tsx
17
+ * <I18nProvider i18n={i18n}>
18
+ * <App />
19
+ * </I18nProvider>
20
+ * ```
21
+ *
22
+ * Test setups that cannot wrap a provider bind the same instance here so
23
+ * ui components still resolve it.
24
+ */
25
+ export { I18nextProvider as I18nProvider, setI18n }
package/src/ui/de.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "caption": {
3
+ "back": "Zurück",
4
+ "forward": "Vorwärts",
5
+ "reload": "Neu laden",
6
+ "minimize": "Minimieren",
7
+ "maximize": "Maximieren",
8
+ "restore": "Wiederherstellen",
9
+ "close": "Schließen",
10
+ "devtools": "Entwicklertools"
11
+ },
12
+ "closeConfirm": {
13
+ "title": "hoardodile schließen?",
14
+ "description": "Die App läuft im Infobereich weiter, außer du beendest sie.",
15
+ "tray": "Im Infobereich ausblenden",
16
+ "quit": "App beenden",
17
+ "cancel": "Abbrechen",
18
+ "remember": "Meine Auswahl merken"
19
+ },
20
+ "pluginDownload": {
21
+ "eyebrow": "Plugin-Download",
22
+ "title": "Diese Datei herunterladen?",
23
+ "description": "{{pluginName}} möchte eine Datei in seinen eigenen Speicherordner herunterladen. Die Datei wird von der unten stehenden URL geladen und nur innerhalb des Plugin-Ordners gespeichert.",
24
+ "urlLabel": "URL",
25
+ "destLabel": "Ziel",
26
+ "sizeLabel": "Größe",
27
+ "unknownSize": "unbekannt",
28
+ "deny": "Ablehnen",
29
+ "allow": "Erlauben",
30
+ "remember": "Für diese Sitzung merken (keine weiteren Rückfragen dieses Plugins)"
31
+ },
32
+ "pagination": {
33
+ "region": "Seitennavigation",
34
+ "prev": "Zurück",
35
+ "next": "Weiter",
36
+ "previous": "Vorherige",
37
+ "goTo": "Gehe zu",
38
+ "jumpToPage": "Seite",
39
+ "morePages": "Weitere Seiten"
40
+ },
41
+ "aria": {
42
+ "loading": "Wird geladen",
43
+ "closeToast": "Benachrichtigung schließen",
44
+ "close": "Schließen",
45
+ "more": "Mehr",
46
+ "toggleSidebar": "Seitenleiste umschalten",
47
+ "mobileSidebarDescription": "Zeigt die mobile Seitenleiste.",
48
+ "sidebar": "Seitenleiste",
49
+ "breadcrumb": "Breadcrumb"
50
+ },
51
+ "dialog": {
52
+ "cancel": "Abbrechen"
53
+ },
54
+ "confirmByTyping": {
55
+ "prompt": "Tippe „<name>{{name}}</name>“ ein, um zu bestätigen"
56
+ },
57
+ "colorPicker": {
58
+ "clear": "Farbe entfernen",
59
+ "addPreset": "Zu meinen Presets hinzufügen",
60
+ "removePresetAria": "Preset {{color}} entfernen",
61
+ "customSwatch": "Benutzerdefinierte Farbe wählen"
62
+ },
63
+ "fontPicker": {
64
+ "inherit": "Globale Schriftart übernehmen",
65
+ "inheritedHint": "Übernimmt derzeit die globale Schriftarteinstellung",
66
+ "addCustom": "Schriftart per Name hinzufügen, Eingabetaste drücken…",
67
+ "selected": "Fallback-Reihenfolge — zum Sortieren ziehen",
68
+ "description": "Oberfläche und Listen — Dokumente behalten ihre Lesestimme."
69
+ },
70
+ "imageCrop": {
71
+ "saving": "Speichern…",
72
+ "remove": "Entfernen",
73
+ "save": "Speichern",
74
+ "pickHint": "Klicken oder ablegen",
75
+ "reselect": "Erneut hochladen",
76
+ "previewLabel": "Vorschau",
77
+ "previewAlt": "Zugeschnittene Vorschau",
78
+ "showPreview": "Vorschau anzeigen",
79
+ "saveFailed": "Speichern fehlgeschlagen"
80
+ },
81
+ "panelToolbar": {
82
+ "add": "Hinzufügen",
83
+ "unused": "Ungenutzt",
84
+ "reorder": "Neu ordnen"
85
+ }
86
+ }
package/src/ui/en.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "caption": {
3
+ "back": "Back",
4
+ "forward": "Forward",
5
+ "reload": "Reload",
6
+ "minimize": "Minimize",
7
+ "maximize": "Maximize",
8
+ "restore": "Restore",
9
+ "close": "Close",
10
+ "devtools": "Developer tools"
11
+ },
12
+ "closeConfirm": {
13
+ "title": "Close hoardodile?",
14
+ "description": "The app keeps running in the tray unless you quit it.",
15
+ "tray": "Hide to tray",
16
+ "quit": "Quit the app",
17
+ "cancel": "Cancel",
18
+ "remember": "Remember my choice"
19
+ },
20
+ "pluginDownload": {
21
+ "eyebrow": "Plugin download",
22
+ "title": "Download this file?",
23
+ "description": "{{pluginName}} wants to download a file into its own storage folder. The file is fetched from the URL below and stored only inside the plugin's folder.",
24
+ "urlLabel": "URL",
25
+ "destLabel": "Destination",
26
+ "sizeLabel": "Size",
27
+ "unknownSize": "unknown",
28
+ "deny": "Deny",
29
+ "allow": "Allow",
30
+ "remember": "Remember for this session (no more prompts from this plugin)"
31
+ },
32
+ "pagination": {
33
+ "region": "pagination",
34
+ "prev": "Prev",
35
+ "next": "Next",
36
+ "previous": "Previous",
37
+ "goTo": "Go to",
38
+ "jumpToPage": "Page",
39
+ "morePages": "More pages"
40
+ },
41
+ "aria": {
42
+ "loading": "Loading",
43
+ "closeToast": "Close toast",
44
+ "close": "Close",
45
+ "more": "More",
46
+ "toggleSidebar": "Toggle Sidebar",
47
+ "mobileSidebarDescription": "Displays the mobile sidebar.",
48
+ "sidebar": "Sidebar",
49
+ "breadcrumb": "breadcrumb"
50
+ },
51
+ "dialog": {
52
+ "cancel": "Cancel"
53
+ },
54
+ "confirmByTyping": {
55
+ "prompt": "Type \"<name>{{name}}</name>\" to confirm"
56
+ },
57
+ "colorPicker": {
58
+ "clear": "Clear color",
59
+ "addPreset": "Add to my presets",
60
+ "removePresetAria": "Remove preset {{color}}",
61
+ "customSwatch": "Pick a custom color"
62
+ },
63
+ "fontPicker": {
64
+ "inherit": "Inherit global font",
65
+ "inheritedHint": "Currently inheriting the global font setting",
66
+ "addCustom": "Add a font by name, press Enter…",
67
+ "selected": "Fallback order — drag to rearrange",
68
+ "description": "Chrome and lists — documents keep their reading voice."
69
+ },
70
+ "imageCrop": {
71
+ "saving": "Saving…",
72
+ "remove": "Remove",
73
+ "save": "Save",
74
+ "pickHint": "Click or drop",
75
+ "reselect": "Re-upload",
76
+ "previewLabel": "Preview",
77
+ "previewAlt": "Cropped preview",
78
+ "showPreview": "Show preview",
79
+ "saveFailed": "Save failed"
80
+ },
81
+ "panelToolbar": {
82
+ "add": "Add",
83
+ "unused": "Unused",
84
+ "reorder": "Reorder"
85
+ }
86
+ }