mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
fix(client): remove Hungarian and Korean locales
Both catalogs held nothing but English fallback values, so selecting either language produced a fully English interface that reads as broken rather than untranslated. Crowdin Translate covers only 24 languages and includes neither, so neither locale had any machine translation route, and no human translations existed yet. Drop them from the shared locale list, the Crowdin export configuration, the pull request allowlist, the catalogs, and the tests, leaving fifteen target languages. Remove both from the Crowdin project as well, after confirming each export was byte-identical to the English source so no translation work was discarded. Re-adding either is cheap once a translation route exists, whether that is a machine translation engine that supports them or a volunteer translator. Give the locale tests the same fallback semantics the application runs with. Production always loads the English catalog and points `fallbackLocale` at it, so a key a target catalog does not carry renders English. The test instances omitted both, which resolves a missing key to its own name, and several assertions compared a translated label against the English one without checking that the language actually translates that key. Both patterns pass only while every catalog is complete, and would fail confusingly the first time a language lags behind a new key. Delete the Crowdin service branch automatically once its pull request closes or merges. Crowdin appends every export to the same branch and never rebases it, so a branch that outlives its pull request keeps its original merge base and each later export diverges further from main until every catalog conflicts. Deleting it forces Crowdin to recreate the branch from the current main. Ref #576
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
name: Crowdin Branch Cleanup
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
delete-service-branch:
|
||||||
|
# Crowdin appends every export to the same service branch and never rebases it.
|
||||||
|
# A branch that outlives its pull request keeps its original merge base, so each
|
||||||
|
# later export diverges further from main and eventually conflicts on every
|
||||||
|
# catalog. Deleting it forces Crowdin to recreate it from the current main.
|
||||||
|
# This runs on close as well as merge, because Crowdin reuses the branch either way.
|
||||||
|
if: github.event.pull_request.head.ref == 'l10n_main' && github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
name: Delete l10n_main
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Delete the Crowdin service branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: l10n_main
|
||||||
|
run: |
|
||||||
|
if gh api "repos/$REPO/git/refs/heads/$BRANCH" >/dev/null 2>&1; then
|
||||||
|
gh api -X DELETE "repos/$REPO/git/refs/heads/$BRANCH"
|
||||||
|
echo "Deleted $BRANCH; Crowdin will recreate it from main on the next export."
|
||||||
|
else
|
||||||
|
echo "$BRANCH is already absent; nothing to delete."
|
||||||
|
fi
|
||||||
@@ -7,6 +7,13 @@ import { setI18nLocale } from '@/i18n'
|
|||||||
import en from '@/locales/en.json'
|
import en from '@/locales/en.json'
|
||||||
import pt from '@/locales/pt.json'
|
import pt from '@/locales/pt.json'
|
||||||
|
|
||||||
|
// Production falls back to English for any key a target catalog has not translated,
|
||||||
|
// so these are the labels Portuguese actually renders. Reading pt directly would
|
||||||
|
// assert `undefined` against rendered English the first time Portuguese lags
|
||||||
|
// behind a new dashboard key.
|
||||||
|
const PT_WIDGET_NAMES = { ...en.dashboard.settings.widgetNames, ...pt.dashboard.settings.widgetNames }
|
||||||
|
const PT_SHELF_NAMES = { ...en.dashboard.settings.shelfNames, ...pt.dashboard.settings.shelfNames }
|
||||||
|
|
||||||
type UseSmartScopesMock = () => {
|
type UseSmartScopesMock = () => {
|
||||||
smartScopes: Ref<unknown[]>
|
smartScopes: Ref<unknown[]>
|
||||||
fetchSmartScopes: () => void
|
fetchSmartScopes: () => void
|
||||||
@@ -113,7 +120,7 @@ describe('DashboardSettingsSheet', () => {
|
|||||||
await setI18nLocale('pt')
|
await setI18nLocale('pt')
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
expect(shelfOptionLabels(wrapper).sort()).toEqual(Object.values(pt.dashboard.settings.shelfNames).sort())
|
expect(shelfOptionLabels(wrapper).sort()).toEqual(Object.values(PT_SHELF_NAMES).sort())
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders every widget name from the catalog rather than a hardcoded map', async () => {
|
it('renders every widget name from the catalog rather than a hardcoded map', async () => {
|
||||||
@@ -146,20 +153,27 @@ describe('DashboardSettingsSheet', () => {
|
|||||||
const labels = widgetRowLabels(wrapper)
|
const labels = widgetRowLabels(wrapper)
|
||||||
|
|
||||||
expect(labels).toEqual([
|
expect(labels).toEqual([
|
||||||
pt.dashboard.settings.widgetNames.readingStreak,
|
PT_WIDGET_NAMES.readingStreak,
|
||||||
pt.dashboard.settings.widgetNames.currentlyReading,
|
PT_WIDGET_NAMES.currentlyReading,
|
||||||
pt.dashboard.settings.widgetNames.readingGoal,
|
PT_WIDGET_NAMES.readingGoal,
|
||||||
pt.dashboard.settings.widgetNames.readingDna,
|
PT_WIDGET_NAMES.readingDna,
|
||||||
pt.dashboard.settings.widgetNames.monthlyChallenge,
|
PT_WIDGET_NAMES.monthlyChallenge,
|
||||||
pt.dashboard.settings.widgetNames.highlightOfTheDay,
|
PT_WIDGET_NAMES.highlightOfTheDay,
|
||||||
pt.dashboard.settings.widgetNames.neglectedGems,
|
PT_WIDGET_NAMES.neglectedGems,
|
||||||
pt.dashboard.settings.widgetNames.readingRhythm,
|
PT_WIDGET_NAMES.readingRhythm,
|
||||||
pt.dashboard.settings.widgetNames.diversityScore,
|
PT_WIDGET_NAMES.diversityScore,
|
||||||
pt.dashboard.settings.widgetNames.libraryOverview,
|
PT_WIDGET_NAMES.libraryOverview,
|
||||||
pt.dashboard.settings.widgetNames.yearProjection,
|
PT_WIDGET_NAMES.yearProjection,
|
||||||
pt.dashboard.settings.widgetNames.longWait,
|
PT_WIDGET_NAMES.longWait,
|
||||||
])
|
])
|
||||||
expect(labels).not.toContain(en.dashboard.settings.widgetNames.readingStreak)
|
|
||||||
expect(labels).not.toContain(en.dashboard.settings.widgetNames.currentlyReading)
|
// Only meaningful for names Portuguese actually translates. A name that falls
|
||||||
|
// back to English renders the English label legitimately, so asserting its
|
||||||
|
// absence would fail for a reason that is not a bug.
|
||||||
|
const englishNamesPortugueseReplaces = (['readingStreak', 'currentlyReading'] as const)
|
||||||
|
.filter((key) => PT_WIDGET_NAMES[key] !== en.dashboard.settings.widgetNames[key])
|
||||||
|
.map((key) => en.dashboard.settings.widgetNames[key])
|
||||||
|
|
||||||
|
expect(labels.filter((label) => englishNamesPortugueseReplaces.includes(label))).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,9 +24,16 @@ function mountComposable(): ReturnType<typeof useDashboardLabels> {
|
|||||||
|
|
||||||
type CatalogNames = { widgetNames: Record<string, string>; shelfNames: Record<string, string> }
|
type CatalogNames = { widgetNames: Record<string, string>; shelfNames: Record<string, string> }
|
||||||
|
|
||||||
|
// Production falls back to English for any key a target catalog has not translated,
|
||||||
|
// so the expected label is the Portuguese one where it exists and English otherwise.
|
||||||
|
// Reading pt directly would assert `undefined` against rendered English the first
|
||||||
|
// time Portuguese lags behind a new dashboard key.
|
||||||
const CATALOGS: Record<'en' | 'pt', CatalogNames> = {
|
const CATALOGS: Record<'en' | 'pt', CatalogNames> = {
|
||||||
en: en.dashboard.settings,
|
en: en.dashboard.settings,
|
||||||
pt: pt.dashboard.settings,
|
pt: {
|
||||||
|
widgetNames: { ...en.dashboard.settings.widgetNames, ...pt.dashboard.settings.widgetNames },
|
||||||
|
shelfNames: { ...en.dashboard.settings.shelfNames, ...pt.dashboard.settings.shelfNames },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors the kebab-case type to camel-case message key mapping the composable owns.
|
// Mirrors the kebab-case type to camel-case message key mapping the composable owns.
|
||||||
@@ -46,6 +53,13 @@ const WIDGET_KEY_BY_TYPE: Record<WidgetType, string> = {
|
|||||||
'long-wait': 'longWait',
|
'long-wait': 'longWait',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A widget whose Portuguese name genuinely differs from English. Assertions that a
|
||||||
|
// translated label is not the English one only hold for such a widget: a name that
|
||||||
|
// falls back to English renders the English string for a legitimate reason.
|
||||||
|
const PT_TRANSLATED_WIDGET = (Object.keys(WIDGET_KEY_BY_TYPE) as WidgetType[]).find(
|
||||||
|
(type) => CATALOGS.pt.widgetNames[WIDGET_KEY_BY_TYPE[type]] !== CATALOGS.en.widgetNames[WIDGET_KEY_BY_TYPE[type]],
|
||||||
|
)
|
||||||
|
|
||||||
const SHELF_KEY_BY_TYPE: Record<ScrollerType, string> = {
|
const SHELF_KEY_BY_TYPE: Record<ScrollerType, string> = {
|
||||||
'recently-added': 'recentlyAdded',
|
'recently-added': 'recentlyAdded',
|
||||||
'continue-reading': 'continueReading',
|
'continue-reading': 'continueReading',
|
||||||
@@ -107,19 +121,23 @@ describe('useDashboardLabels', () => {
|
|||||||
const key = WIDGET_KEY_BY_TYPE[type]
|
const key = WIDGET_KEY_BY_TYPE[type]
|
||||||
expect(widgetName(type), `widget name for ${type}`).toBe(CATALOGS.pt.widgetNames[key])
|
expect(widgetName(type), `widget name for ${type}`).toBe(CATALOGS.pt.widgetNames[key])
|
||||||
}
|
}
|
||||||
expect(widgetName('reading-streak')).not.toBe(CATALOGS.en.widgetNames.readingStreak)
|
expect(PT_TRANSLATED_WIDGET, 'Portuguese should translate at least one widget name').toBeDefined()
|
||||||
expect(widgetName('currently-reading')).not.toBe(CATALOGS.en.widgetNames.currentlyReading)
|
const translatedKey = WIDGET_KEY_BY_TYPE[PT_TRANSLATED_WIDGET!]
|
||||||
|
expect(widgetName(PT_TRANSLATED_WIDGET!)).not.toBe(CATALOGS.en.widgetNames[translatedKey])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('re-resolves when the locale changes back', async () => {
|
it('re-resolves when the locale changes back', async () => {
|
||||||
const { widgetName } = mountComposable()
|
const { widgetName } = mountComposable()
|
||||||
|
|
||||||
|
expect(PT_TRANSLATED_WIDGET, 'Portuguese should translate at least one widget name').toBeDefined()
|
||||||
|
const type = PT_TRANSLATED_WIDGET!
|
||||||
|
|
||||||
await setI18nLocale('pt')
|
await setI18nLocale('pt')
|
||||||
const translated = widgetName('reading-streak')
|
const translated = widgetName(type)
|
||||||
await setI18nLocale('en')
|
await setI18nLocale('en')
|
||||||
|
|
||||||
expect(translated).not.toBe(widgetName('reading-streak'))
|
expect(translated).not.toBe(widgetName(type))
|
||||||
expect(widgetName('reading-streak')).toBe(CATALOGS.en.widgetNames.readingStreak)
|
expect(widgetName(type)).toBe(CATALOGS.en.widgetNames[WIDGET_KEY_BY_TYPE[type]])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+23
-55
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import type { Locale } from '@bookorbit/types'
|
||||||
import cs from '@/locales/cs.json'
|
import cs from '@/locales/cs.json'
|
||||||
import da from '@/locales/da.json'
|
import da from '@/locales/da.json'
|
||||||
import de from '@/locales/de.json'
|
import de from '@/locales/de.json'
|
||||||
@@ -7,9 +8,7 @@ import en from '@/locales/en.json'
|
|||||||
import fi from '@/locales/fi.json'
|
import fi from '@/locales/fi.json'
|
||||||
import spanish from '@/locales/es.json'
|
import spanish from '@/locales/es.json'
|
||||||
import french from '@/locales/fr.json'
|
import french from '@/locales/fr.json'
|
||||||
import hu from '@/locales/hu.json'
|
|
||||||
import italian from '@/locales/it.json'
|
import italian from '@/locales/it.json'
|
||||||
import ko from '@/locales/ko.json'
|
|
||||||
import nl from '@/locales/nl.json'
|
import nl from '@/locales/nl.json'
|
||||||
import polish from '@/locales/pl.json'
|
import polish from '@/locales/pl.json'
|
||||||
import pt from '@/locales/pt.json'
|
import pt from '@/locales/pt.json'
|
||||||
@@ -35,6 +34,19 @@ function messageValues(message: string, count: number): Record<string, string |
|
|||||||
return Object.fromEntries(argumentNames.map((name) => [name, name === 'count' ? count : 2]))
|
return Object.fromEntries(argumentNames.map((name) => [name, name === 'count' ? count : 2]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirrors the production setup in `@/i18n`, where the English catalog is always
|
||||||
|
// loaded and `fallbackLocale` points at it. Without that, a key absent from a
|
||||||
|
// target catalog resolves to the key name rather than English, so a catalog
|
||||||
|
// carrying only translated keys would fail these tests instead of falling back
|
||||||
|
// the way the running application does.
|
||||||
|
function createCatalogI18n(locale: Locale, catalog: typeof en) {
|
||||||
|
const messages: Record<string, typeof en> = {
|
||||||
|
en: compileIcuCatalog(en, 'en'),
|
||||||
|
[locale]: compileIcuCatalog(catalog, locale),
|
||||||
|
}
|
||||||
|
return createI18n({ legacy: false, locale, fallbackLocale: 'en', messages })
|
||||||
|
}
|
||||||
|
|
||||||
describe('ICU message compilation', () => {
|
describe('ICU message compilation', () => {
|
||||||
it('detects ICU plurals without treating ordinary interpolation as ICU', () => {
|
it('detects ICU plurals without treating ordinary interpolation as ICU', () => {
|
||||||
expect(isIcuPluralMessage('{count, plural, one {One book} other {# books}}')).toBe(true)
|
expect(isIcuPluralMessage('{count, plural, one {One book} other {# books}}')).toBe(true)
|
||||||
@@ -92,12 +104,7 @@ describe('ICU message compilation', () => {
|
|||||||
['pt', pt, [0, 1, 2, 1_234]],
|
['pt', pt, [0, 1, 2, 1_234]],
|
||||||
['sl', sl, [0, 1, 2, 3, 5, 1_234]],
|
['sl', sl, [0, 1, 2, 3, 5, 1_234]],
|
||||||
] as const)('isolates the styled count in both dialogs for %s', (locale, catalog, counts) => {
|
] as const)('isolates the styled count in both dialogs for %s', (locale, catalog, counts) => {
|
||||||
const messages = compileIcuCatalog(catalog, locale)
|
const testI18n = createCatalogI18n(locale, catalog)
|
||||||
const testI18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale,
|
|
||||||
messages: { [locale]: messages },
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const key of ['tools.bulkRename.confirmDialog.body', 'tools.entityManager.bulkDeleteModal.confirm']) {
|
for (const key of ['tools.bulkRename.confirmDialog.body', 'tools.entityManager.bulkDeleteModal.confirm']) {
|
||||||
for (const count of counts) {
|
for (const count of counts) {
|
||||||
@@ -141,16 +148,8 @@ describe('ICU message compilation', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders reviewed Italian and Polish count messages with the correct grammar', () => {
|
it('renders reviewed Italian and Polish count messages with the correct grammar', () => {
|
||||||
const italianI18n = createI18n({
|
const italianI18n = createCatalogI18n('it', italian)
|
||||||
legacy: false,
|
const polishI18n = createCatalogI18n('pl', polish)
|
||||||
locale: 'it',
|
|
||||||
messages: { it: compileIcuCatalog(italian, 'it') },
|
|
||||||
})
|
|
||||||
const polishI18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale: 'pl',
|
|
||||||
messages: { pl: compileIcuCatalog(polish, 'pl') },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(italianI18n.global.t('settings.appearance.icons.selectedCount', { count: 1 })).toBe('1 icona selezionata')
|
expect(italianI18n.global.t('settings.appearance.icons.selectedCount', { count: 1 })).toBe('1 icona selezionata')
|
||||||
expect(italianI18n.global.t('settings.appearance.icons.selectedCount', { count: 2 })).toBe('2 icone selezionate')
|
expect(italianI18n.global.t('settings.appearance.icons.selectedCount', { count: 2 })).toBe('2 icone selezionate')
|
||||||
@@ -186,11 +185,7 @@ describe('ICU message compilation', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('preserves spacing around collection names in reviewed Italian messages', () => {
|
it('preserves spacing around collection names in reviewed Italian messages', () => {
|
||||||
const testI18n = createI18n({
|
const testI18n = createCatalogI18n('it', italian)
|
||||||
legacy: false,
|
|
||||||
locale: 'it',
|
|
||||||
messages: { it: compileIcuCatalog(italian, 'it') },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(testI18n.global.t('collection.addToSheet.createdAndAdded', { count: 2, name: 'Preferiti' })).toBe('Creata "Preferiti" e aggiunti 2 libri')
|
expect(testI18n.global.t('collection.addToSheet.createdAndAdded', { count: 2, name: 'Preferiti' })).toBe('Creata "Preferiti" e aggiunti 2 libri')
|
||||||
expect(testI18n.global.t('collection.addToSheet.addedToCollection', { count: 2, name: 'Preferiti' })).toBe('Aggiunti 2 libri a "Preferiti"')
|
expect(testI18n.global.t('collection.addToSheet.addedToCollection', { count: 2, name: 'Preferiti' })).toBe('Aggiunti 2 libri a "Preferiti"')
|
||||||
@@ -212,12 +207,7 @@ describe('ICU message compilation', () => {
|
|||||||
['pt', pt, [0, 1, 1_000_000], ['Sem grupos duplicados', 'Um grupo duplicado', '1.000.000 grupos duplicados']],
|
['pt', pt, [0, 1, 1_000_000], ['Sem grupos duplicados', 'Um grupo duplicado', '1.000.000 grupos duplicados']],
|
||||||
['sl', sl, [0, 1, 2, 3, 5], ['No duplicate groups', 'One duplicate group', '2 duplicate groups', '3 duplicate groups', '5 duplicate groups']],
|
['sl', sl, [0, 1, 2, 3, 5], ['No duplicate groups', 'One duplicate group', '2 duplicate groups', '3 duplicate groups', '5 duplicate groups']],
|
||||||
] as const)('renders migrated Book Duplicates messages for %s', (locale, catalog, counts, expectedGroups) => {
|
] as const)('renders migrated Book Duplicates messages for %s', (locale, catalog, counts, expectedGroups) => {
|
||||||
const messages = compileIcuCatalog(catalog, locale)
|
const testI18n = createCatalogI18n(locale, catalog)
|
||||||
const testI18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale,
|
|
||||||
messages: { [locale]: messages },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(counts.map((count) => testI18n.global.t('tools.bookDuplicates.groupsFound', { count }))).toEqual(expectedGroups)
|
expect(counts.map((count) => testI18n.global.t('tools.bookDuplicates.groupsFound', { count }))).toEqual(expectedGroups)
|
||||||
|
|
||||||
@@ -245,12 +235,7 @@ describe('ICU message compilation', () => {
|
|||||||
['pt', pt, [1, 1_000_000], ['1 falha', '1.000.000 falhas']],
|
['pt', pt, [1, 1_000_000], ['1 falha', '1.000.000 falhas']],
|
||||||
['sl', sl, [1, 2, 3, 5], ['1 napaka', '2 napaki', '3 napak', '5 napak']],
|
['sl', sl, [1, 2, 3, 5], ['1 napaka', '2 napaki', '3 napak', '5 napak']],
|
||||||
] as const)('renders migrated Kobo activity plurals for %s', (locale, catalog, counts, expected) => {
|
] as const)('renders migrated Kobo activity plurals for %s', (locale, catalog, counts, expected) => {
|
||||||
const messages = compileIcuCatalog(catalog, locale)
|
const testI18n = createCatalogI18n(locale, catalog)
|
||||||
const testI18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale,
|
|
||||||
messages: { [locale]: messages },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(counts.map((count) => testI18n.global.t('settings.reader.kobo.activity.failureCount', { count }))).toEqual(expected)
|
expect(counts.map((count) => testI18n.global.t('settings.reader.kobo.activity.failureCount', { count }))).toEqual(expected)
|
||||||
})
|
})
|
||||||
@@ -276,23 +261,13 @@ describe('ICU message compilation', () => {
|
|||||||
['0 datotek pripravljenih', '1 datoteka pripravljena', '2 datoteki pripravljeni', '3 datoteke pripravljene', '5 datotek pripravljenih'],
|
['0 datotek pripravljenih', '1 datoteka pripravljena', '2 datoteki pripravljeni', '3 datoteke pripravljene', '5 datotek pripravljenih'],
|
||||||
],
|
],
|
||||||
] as const)('renders migrated finalize-dialog counts for %s', (locale, catalog, counts, expected) => {
|
] as const)('renders migrated finalize-dialog counts for %s', (locale, catalog, counts, expected) => {
|
||||||
const messages = compileIcuCatalog(catalog, locale)
|
const testI18n = createCatalogI18n(locale, catalog)
|
||||||
const testI18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale,
|
|
||||||
messages: { [locale]: messages },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(counts.map((count) => testI18n.global.t('bookDock.finalizeDialog.readyCount', { count }))).toEqual(expected)
|
expect(counts.map((count) => testI18n.global.t('bookDock.finalizeDialog.readyCount', { count }))).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the reviewed Slovenian zero/one/other mappings in the finalize dialog', () => {
|
it('uses the reviewed Slovenian zero/one/other mappings in the finalize dialog', () => {
|
||||||
const messages = compileIcuCatalog(sl, 'sl')
|
const testI18n = createCatalogI18n('sl', sl)
|
||||||
const testI18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale: 'sl',
|
|
||||||
messages: { sl: messages },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(testI18n.global.t('bookDock.finalizeDialog.title', { count: 0 })).toBe('Zaključi brez datotek')
|
expect(testI18n.global.t('bookDock.finalizeDialog.title', { count: 0 })).toBe('Zaključi brez datotek')
|
||||||
expect(testI18n.global.t('bookDock.finalizeDialog.title', { count: 1 })).toBe('Zaključi 1 datoteko')
|
expect(testI18n.global.t('bookDock.finalizeDialog.title', { count: 1 })).toBe('Zaključi 1 datoteko')
|
||||||
@@ -308,10 +283,8 @@ describe('ICU message compilation', () => {
|
|||||||
['de', de, [0, 1, 2]],
|
['de', de, [0, 1, 2]],
|
||||||
['es', spanish, [0, 1, 2, 1_000_000]],
|
['es', spanish, [0, 1, 2, 1_000_000]],
|
||||||
['fi', fi, [0, 1, 2]],
|
['fi', fi, [0, 1, 2]],
|
||||||
['hu', hu, [0, 1, 2]],
|
|
||||||
['fr', french, [0, 1, 2, 1_000_000]],
|
['fr', french, [0, 1, 2, 1_000_000]],
|
||||||
['it', italian, [0, 1, 2, 1_000_000]],
|
['it', italian, [0, 1, 2, 1_000_000]],
|
||||||
['ko', ko, [0, 1, 2]],
|
|
||||||
['nl', nl, [0, 1, 2]],
|
['nl', nl, [0, 1, 2]],
|
||||||
// Polish reserves `other` for fractions, exactly as Czech does.
|
// Polish reserves `other` for fractions, exactly as Czech does.
|
||||||
['pl', polish, [0, 1, 2, 5, 1.5, 1_000_000]],
|
['pl', polish, [0, 1, 2, 5, 1.5, 1_000_000]],
|
||||||
@@ -323,12 +296,7 @@ describe('ICU message compilation', () => {
|
|||||||
['uk', uk, [0, 1, 2, 5, 1.5]],
|
['uk', uk, [0, 1, 2, 5, 1.5]],
|
||||||
['zh', zh, [0, 1, 2]],
|
['zh', zh, [0, 1, 2]],
|
||||||
] as const)('formats every ICU message for all relevant plural categories in %s', (locale, catalog, counts) => {
|
] as const)('formats every ICU message for all relevant plural categories in %s', (locale, catalog, counts) => {
|
||||||
const messages = compileIcuCatalog(catalog, locale)
|
const testI18n = createCatalogI18n(locale, catalog)
|
||||||
const testI18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale,
|
|
||||||
messages: { [locale]: messages },
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const [key, message] of flattenMessages(catalog)) {
|
for (const [key, message] of flattenMessages(catalog)) {
|
||||||
if (!isIcuPluralMessage(message)) continue
|
if (!isIcuPluralMessage(message)) continue
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -42,8 +42,6 @@ describe('locale store', () => {
|
|||||||
expect(matchSupportedLocale(['cs-CZ', 'en-GB'])).toBe('cs')
|
expect(matchSupportedLocale(['cs-CZ', 'en-GB'])).toBe('cs')
|
||||||
expect(matchSupportedLocale(['da-DK', 'en-GB'])).toBe('da')
|
expect(matchSupportedLocale(['da-DK', 'en-GB'])).toBe('da')
|
||||||
expect(matchSupportedLocale(['fi-FI', 'en-GB'])).toBe('fi')
|
expect(matchSupportedLocale(['fi-FI', 'en-GB'])).toBe('fi')
|
||||||
expect(matchSupportedLocale(['hu-HU', 'en-GB'])).toBe('hu')
|
|
||||||
expect(matchSupportedLocale(['ko-KR', 'en-GB'])).toBe('ko')
|
|
||||||
expect(matchSupportedLocale(['sv-SE', 'en-GB'])).toBe('sv')
|
expect(matchSupportedLocale(['sv-SE', 'en-GB'])).toBe('sv')
|
||||||
expect(matchSupportedLocale(['sv-FI', 'en-GB'])).toBe('sv')
|
expect(matchSupportedLocale(['sv-FI', 'en-GB'])).toBe('sv')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ export_languages:
|
|||||||
- es-ES
|
- es-ES
|
||||||
- fi
|
- fi
|
||||||
- fr
|
- fr
|
||||||
- hu
|
|
||||||
- it
|
- it
|
||||||
- ko
|
|
||||||
- nl
|
- nl
|
||||||
- pl
|
- pl
|
||||||
- pt-BR
|
- pt-BR
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ BookOrbit uses Vue I18n catalogs under `client/src/locales/`. English is the sou
|
|||||||
| Spanish | `es-ES` | `es.json` |
|
| Spanish | `es-ES` | `es.json` |
|
||||||
| Finnish | `fi` | `fi.json` |
|
| Finnish | `fi` | `fi.json` |
|
||||||
| French | `fr` | `fr.json` |
|
| French | `fr` | `fr.json` |
|
||||||
| Hungarian | `hu` | `hu.json` |
|
|
||||||
| Italian | `it` | `it.json` |
|
| Italian | `it` | `it.json` |
|
||||||
| Korean | `ko` | `ko.json` |
|
|
||||||
| Dutch | `nl` | `nl.json` |
|
| Dutch | `nl` | `nl.json` |
|
||||||
| Polish | `pl` | `pl.json` |
|
| Polish | `pl` | `pl.json` |
|
||||||
| Brazilian Portuguese | `pt-BR` | `pt.json` |
|
| Brazilian Portuguese | `pt-BR` | `pt.json` |
|
||||||
@@ -89,9 +87,11 @@ After an English source change reaches `main`:
|
|||||||
2. Translators update target strings in Crowdin.
|
2. Translators update target strings in Crowdin.
|
||||||
3. Crowdin exports the target catalogs to `l10n_main`.
|
3. Crowdin exports the target catalogs to `l10n_main`.
|
||||||
4. Crowdin opens a pull request to `main`.
|
4. Crowdin opens a pull request to `main`.
|
||||||
5. CI verifies that the pull request changes only the seventeen target catalogs and runs the normal client checks.
|
5. CI verifies that the pull request changes only the fifteen target catalogs and runs the normal client checks.
|
||||||
6. A maintainer reviews and squash-merges the pull request.
|
6. A maintainer reviews and squash-merges the pull request.
|
||||||
7. The `l10n_main` service branch is deleted. Crowdin recreates it for the next export.
|
7. The `.github/workflows/crowdin-branch-cleanup.yml` workflow deletes the `l10n_main` service branch. Crowdin recreates it from the current `main` for the next export.
|
||||||
|
|
||||||
|
Deleting that branch is required, not tidiness. Crowdin appends every export to the same service branch and never rebases it, so a branch that outlives its pull request keeps its original merge base. Each later export then diverges further from `main` and eventually conflicts on every catalog. The workflow runs when a pull request from `l10n_main` is closed as well as merged, because Crowdin reuses the branch either way.
|
||||||
|
|
||||||
Crowdin pull requests must retain the configured `i18n(client)` title and commit format. The `i18n` commit type produces a patch release and an Internationalization release-note section. Crowdin's default `[ci skip]` commit suffix is disabled so pull-request validation runs before merge.
|
Crowdin pull requests must retain the configured `i18n(client)` title and commit format. The `i18n` commit type produces a patch release and an Internationalization release-note section. Crowdin's default `[ci skip]` commit suffix is disabled so pull-request validation runs before merge.
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
export const SUPPORTED_LOCALES = [
|
export const SUPPORTED_LOCALES = ["en", "cs", "da", "de", "es", "fi", "fr", "it", "nl", "pl", "pt", "ru", "sl", "sv", "uk", "zh"] as const;
|
||||||
"en",
|
|
||||||
"cs",
|
|
||||||
"da",
|
|
||||||
"de",
|
|
||||||
"es",
|
|
||||||
"fi",
|
|
||||||
"fr",
|
|
||||||
"hu",
|
|
||||||
"it",
|
|
||||||
"ko",
|
|
||||||
"nl",
|
|
||||||
"pl",
|
|
||||||
"pt",
|
|
||||||
"ru",
|
|
||||||
"sl",
|
|
||||||
"sv",
|
|
||||||
"uk",
|
|
||||||
"zh",
|
|
||||||
] as const;
|
|
||||||
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||||
|
|
||||||
export const DEFAULT_LOCALE: Locale = "en";
|
export const DEFAULT_LOCALE: Locale = "en";
|
||||||
@@ -31,9 +12,7 @@ export const LOCALE_LABELS: Record<Locale, string> = {
|
|||||||
es: "Español",
|
es: "Español",
|
||||||
fi: "Suomi",
|
fi: "Suomi",
|
||||||
fr: "Français",
|
fr: "Français",
|
||||||
hu: "Magyar",
|
|
||||||
it: "Italiano",
|
it: "Italiano",
|
||||||
ko: "한국어",
|
|
||||||
nl: "Nederlands",
|
nl: "Nederlands",
|
||||||
pl: "Polski",
|
pl: "Polski",
|
||||||
pt: "Português",
|
pt: "Português",
|
||||||
@@ -52,9 +31,7 @@ export const LOCALE_DIRECTIONS: Record<Locale, "ltr" | "rtl"> = {
|
|||||||
es: "ltr",
|
es: "ltr",
|
||||||
fi: "ltr",
|
fi: "ltr",
|
||||||
fr: "ltr",
|
fr: "ltr",
|
||||||
hu: "ltr",
|
|
||||||
it: "ltr",
|
it: "ltr",
|
||||||
ko: "ltr",
|
|
||||||
nl: "ltr",
|
nl: "ltr",
|
||||||
pl: "ltr",
|
pl: "ltr",
|
||||||
pt: "ltr",
|
pt: "ltr",
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ allowed_paths=(
|
|||||||
"client/src/locales/es.json"
|
"client/src/locales/es.json"
|
||||||
"client/src/locales/fi.json"
|
"client/src/locales/fi.json"
|
||||||
"client/src/locales/fr.json"
|
"client/src/locales/fr.json"
|
||||||
"client/src/locales/hu.json"
|
|
||||||
"client/src/locales/it.json"
|
"client/src/locales/it.json"
|
||||||
"client/src/locales/ko.json"
|
|
||||||
"client/src/locales/nl.json"
|
"client/src/locales/nl.json"
|
||||||
"client/src/locales/pl.json"
|
"client/src/locales/pl.json"
|
||||||
"client/src/locales/pt.json"
|
"client/src/locales/pt.json"
|
||||||
|
|||||||
Reference in New Issue
Block a user