diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3659d..d51c8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.6.0 + +- **FIX**: `localize` skipped any sheet whose base language was not English — a `label | description | meta | ru | ...` header was rejected as "not a localization sheet". The fourth column is now accepted as the source language whenever it is a recognized language code (`ru`, `de`, `pt_BR`, …); non-language data columns (e.g. a `Family` reference table) are still skipped. The source language is carried through into the model prompt and JSON schema, so rows are translated *from* the actual base language instead of always "from English". +- **FIX**: `generate` hard-coded the `flutter gen-l10n` template to `_en.arb`, so a bucket without an English column failed to generate. The template is now chosen per bucket — English when present (unchanged behaviour), otherwise the first available ARB — so a non-English base language generates too. + ## 0.5.0 - **BREAKING**: `localize` now only writes to a sheet whose header matches `label | description | meta | en | ...`. A sheet whose fourth column is not the English source is skipped with a warning — reference tables kept in the same spreadsheet were previously "translated", overwriting their data. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..47a90f9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,48 @@ +# CLAUDE.md + +Guidance for Claude Code when working in this repository. + +## What this package is + +`sheety_localization` is a Dart CLI package that turns a Google Sheet into +localization files. It ships two executables (see `pubspec.yaml`): + +- **`localize`** (`bin/localize.dart`, backed by `lib/src/localize/`) — fills in + missing translations in the sheet via the OpenAI API and writes them back. +- **`generate`** (`bin/generate.dart`) — reads the sheet and generates ARB files + and Flutter localization classes (`flutter gen-l10n`). + +The two share the same sheet layout but are **separate code paths**. The column +layout is `label | description | meta | | ...`: the fourth +column is the base/source language (not necessarily English) and every column +after it is a target locale. When you change how a sheet is interpreted, check +**both** `lib/src/localize/` **and** `bin/generate.dart`. + +## Release hygiene — required for every task/issue + +Every change that touches shippable code MUST leave the package publishable. +Before finishing a task (and before opening a PR), you must: + +1. **Bump the version** in `pubspec.yaml` following semver + (`fix` → patch, new feature/relaxed constraint → minor, breaking → major). +2. **Add a matching entry to `CHANGELOG.md`** under a new version heading, using + the existing `**FIX** / **ADDED** / **CHANGED** / **BREAKING**` style. +3. **Verify the package still publishes** with: + + ```sh + dart pub publish --dry-run + ``` + + It must report no errors (warnings that already existed on `master` are + acceptable, but do not introduce new ones). + +Skipping any of these makes the package impossible to publish, so treat them as +part of "done", not optional follow-up. + +## Checks before opening a PR + +```sh +dart analyze # must be clean +dart test # must be green +dart pub publish --dry-run +``` diff --git a/README.md b/README.md index af0a400..cf2de9d 100644 --- a/README.md +++ b/README.md @@ -421,7 +421,7 @@ dart pub global run sheety_localization:localize \ ### How Localization Failures Are Handled -> **Only localization sheets are written to.** A sheet is localized only when its header matches `label | description | meta | en | ...` — that is, when the fourth column is the English source. Reference tables and notes kept in the same spreadsheet have ordinary data in those columns, and translating them would overwrite it, so they are skipped with a warning. `--ignore` remains available for sheets that *do* match the layout but should be left alone anyway. +> **Only localization sheets are written to.** A sheet is localized only when its header matches `label | description | meta | | ...` — that is, when the fourth column is a recognized source language. It does not have to be English: `ru`, `de`, `pt_BR` and any other known language code work just as well, and the rest of the row is translated *from* that language. Reference tables and notes kept in the same spreadsheet have ordinary data in the fourth column (e.g. `Family`, `Region`), which is not a language, so they are skipped with a warning rather than overwritten with translations. `--ignore` remains available for sheets that *do* match the layout but should be left alone anyway. Language models are unreliable on ambiguous or rare locale codes, so `localize` defends against that: diff --git a/bin/generate.dart b/bin/generate.dart index 6780267..46781f4 100644 --- a/bin/generate.dart +++ b/bin/generate.dart @@ -714,6 +714,24 @@ Future> generateArbFiles({ return files; } +/// Choose the `flutter gen-l10n` template ARB for a single bucket. +/// +/// gen-l10n needs one ARB as the template that defines every key and +/// placeholder. Historically this was hard-coded to `_en.arb`, which +/// breaks a bucket whose base language is not English (no `en` column, so no +/// `_en.arb` is ever written). This prefers the English ARB when it +/// exists — keeping the previous behaviour — and otherwise falls back to the +/// first ARB by name, so a non-English base still generates. +/// +/// [bucketArbs] are the ARB file paths (or names) belonging to one bucket. +String selectTemplateArb(Iterable bucketArbs, {String prefix = 'app'}) { + final names = bucketArbs.map(path.basename).toSet(); + final english = '${prefix}_en.arb'; + if (names.contains(english)) return english; + final sorted = names.toList()..sort(); + return sorted.isEmpty ? english : sorted.first; +} + /// Generate Flutter localization files from the arb files /// flutter gen-l10n --no-synthetic-package \ /// --no-nullable-getter --template-arb-file=app_en.arb \ @@ -773,10 +791,21 @@ Future> generateFlutterLocalization({ final localizations = {}; - final toGenerate = arbs.map((e) => e.parent.absolute.path).toSet(); + // Group the generated ARB files by their bucket directory, so the gen-l10n + // template can be chosen from what each bucket actually has — the base + // language does not have to be English. + final arbsByDir = >{}; + for (final arb in arbs) { + arbsByDir.putIfAbsent(arb.parent.absolute.path, () => []).add(arb); + } + final toGenerate = arbsByDir.keys.toSet(); for (final dir in toGenerate) { final bucket = path.basename(dir); + final template = selectTemplateArb( + arbsByDir[dir]!.map((f) => f.path), + prefix: prefix ?? 'app', + ); final genPath = path.normalize(path.join(genDirectory.path, bucket)); if (!genPath.startsWith(libDirectory.path)) { $err('Gen path is outside of the library directory: $genPath'); @@ -796,7 +825,7 @@ Future> generateFlutterLocalization({ '--no-nullable-getter', // flutter config --explicit-package-dependencies //'--no-synthetic-package', - '--template-arb-file=${prefix ?? 'app'}_en.arb', + '--template-arb-file=$template', '--arb-dir=$dir', '--output-dir=${genDir.path}', '--output-localization-file=$outputFile', diff --git a/lib/src/localize/localizer.dart b/lib/src/localize/localizer.dart index cb0acb3..66a5466 100644 --- a/lib/src/localize/localizer.dart +++ b/lib/src/localize/localizer.dart @@ -13,23 +13,29 @@ import 'validation.dart'; /// Whether [header] describes a localization sheet. /// -/// The expected layout is `label | description | meta | en | ...`, so -/// the fourth column must be the English source. A spreadsheet usually holds -/// other sheets too — reference tables, notes — whose columns are ordinary -/// data. Localizing those would overwrite them with translations, so a sheet -/// that does not match the layout is left alone. +/// The expected layout is `label | description | meta | source | locale ...`, +/// so the fourth column is the source language the rest are translated from. +/// It does not have to be English: `ru`, `de`, `pt_BR` and any other +/// recognized language code are equally valid sources. +/// +/// A spreadsheet usually holds other sheets too — reference tables, notes — +/// whose columns are ordinary data. Localizing those would overwrite them with +/// translations, so a sheet whose fourth column is not a recognized language +/// (e.g. a `Family` or `Region` data column) is left alone. bool isLocalizationHeader(List header) { if (header.length < 5) return false; - final en = header[3]; - if (en is! String) return false; - final code = normalizeLanguageCode(en); - return code == 'en' || code.startsWith('en_'); + final source = header[3]; + if (source is! String) return false; + return resolveLanguageName(source) != null; } /// Extract cells to be localized from the raw [values] of a sheet. /// /// [title] is the sheet title, used for diagnostics only. -/// Column layout: `label | description | meta | en | ...`. +/// Column layout: `label | description | meta | | ...`. +/// The fourth column is the source language (English by default, but any +/// recognized language code is accepted); rows are translated from it into the +/// locale columns that follow. /// A sheet whose header does not match that layout is skipped entirely. List extractEmptyCells({ required String title, @@ -48,12 +54,18 @@ List extractEmptyCells({ if (!isLocalizationHeader(header)) { $err( 'Sheet "$bucket" is not a localization sheet ' - '(expected "label | description | meta | en | ..." header, ' + '(expected "label | description | meta | | ..." ' + 'header with a recognized source language in the fourth column, ' 'got ${header.take(4).toList()}), skipping sheet...', ); return const []; } + // The fourth column is the source language every other column is translated + // from. `isLocalizationHeader` has already vouched for it being a String and + // a recognized language, so the sanitized code is safe to reuse below. + final sourceCode = sanitize(header[3] as String); + // Fill locales final locales = List.filled(header.length, '', growable: false); final seen = {}; @@ -102,8 +114,8 @@ List extractEmptyCells({ continue; } - // Extract label, description, and meta from the row - final [$label, $description, $meta, $english, ..._] = row; + // Extract label, description, meta and source text from the row + final [$label, $description, $meta, $source, ..._] = row; if ($label == null || $label is! String || $label.isEmpty) { $err( 'Sheet "$bucket" has empty label in row #${i + 1}, ' @@ -137,6 +149,7 @@ List extractEmptyCells({ LocalizeRow( row: i, label: label, + sourceCode: sourceCode, description: switch ($description) { String text when text.isNotEmpty => text, num number => number.toString(), @@ -147,7 +160,7 @@ List extractEmptyCells({ num number => number.toString(), _ => null, }, - english: switch ($english) { + source: switch ($source) { String text when text.isNotEmpty => text, num number => number.toString(), _ => label, @@ -191,7 +204,8 @@ Stream localizeRows({ try { final (:prompt, :schema) = buildLocalizationPrompt( label: row.label, - en: row.english, + source: row.source, + sourceCode: row.sourceCode, description: row.description, meta: row.meta, languages: languages, @@ -215,7 +229,7 @@ Stream localizeRows({ _ => null, }; final problem = - validateTranslation(source: row.english, translation: text); + validateTranslation(source: row.source, translation: text); if (problem != null) { $err('Rejected "$code" for "${row.label}": $problem'); failed.add(code); diff --git a/lib/src/localize/models.dart b/lib/src/localize/models.dart index 84ceaf3..22a373d 100644 --- a/lib/src/localize/models.dart +++ b/lib/src/localize/models.dart @@ -31,8 +31,9 @@ class LocalizeRow { required this.label, required this.description, required this.meta, - required this.english, + required this.source, required this.cells, + this.sourceCode = 'en', }); /// Zero-based row index in the sheet. @@ -47,8 +48,14 @@ class LocalizeRow { /// Optional ICU/intl placeholder description. String? meta; - /// English source text. - String english; + /// Language code of the source text, e.g. `en`, `ru`, `pt_BR`. + /// + /// Taken from the fourth column of the sheet header; the whole row is + /// translated *from* this language into the locale columns. + String sourceCode; + + /// Source text, written in the [sourceCode] language. + String source; /// Cells (locales) that have to be localized. List cells; diff --git a/lib/src/localize/prompt.dart b/lib/src/localize/prompt.dart index c79b1c9..203d1ad 100644 --- a/lib/src/localize/prompt.dart +++ b/lib/src/localize/prompt.dart @@ -14,10 +14,16 @@ import 'language_names.dart'; /// /// Every target language is spelled out by name, endonym and — for codes that /// models routinely misread, such as `uk` — an explicit disambiguation note. +/// +/// [source] is the source text, written in the [sourceCode] language. The +/// source language defaults to English but can be any recognized language +/// code (`ru`, `de`, `pt_BR`, …); the prompt and schema name it explicitly so +/// the model translates *from* the right language. ({String prompt, Map schema}) buildLocalizationPrompt({ required String label, - required String en, + required String source, required List languages, + String sourceCode = 'en', String? description, String? meta, // keep as String? to avoid breaking callers; embed as-is }) { @@ -36,13 +42,16 @@ import 'language_names.dart'; // -- unpack & normalize ---------------------------------------------------- final normLabel = safeStr(label); final normDesc = safeStr(description); - final normEn = safeStr(en); + final normSource = safeStr(source); final langs = uniqLangs(languages); final String? metaInline = safeStr(meta); // already a string; embed as-is + // Human-readable name of the source language, used to instruct the model. + final sourceName = resolveLanguageName(sourceCode) ?? sourceCode; + // -- validation (explicit) ------------------------------------------------- if (normLabel == null) throw ArgumentError('Missing label'); - if (normEn == null) throw ArgumentError('Missing source English text'); + if (normSource == null) throw ArgumentError('Missing source text'); if (langs.isEmpty) throw ArgumentError('No target languages provided'); // -- output skeleton (built once; used in prompt to fix structure) --------- @@ -73,7 +82,8 @@ import 'language_names.dart'; p.writeln('meta_placeholders (ICU / intl format): $metaInline'); } p - ..writeln('en_source: $normEn') + ..writeln('source_language: ${describeLanguage(sourceCode)}') + ..writeln('source_text: $normSource') ..writeln('--- TARGET LANGUAGES ---') ..writeln('The JSON keys below are ISO 639-1 / BCP-47 LANGUAGE codes, ' 'never country codes. Translate into the named language, ' @@ -87,7 +97,8 @@ import 'language_names.dart'; p ..writeln('Write each translation in the natural script of that language ' '(e.g. Cyrillic for uk/ru/bg, Devanagari for hi, Arabic for ar).') - ..writeln('Never answer in English for a non-English language code.') + ..writeln('Never answer in $sourceName for a non-$sourceName ' + 'language code.') ..writeln('--- OUTPUT REQUIREMENTS ---') ..writeln( 'Return ONLY valid minified JSON (no comments, no markdown fences).') @@ -97,16 +108,16 @@ import 'language_names.dart'; '(e.g., {name}, {version}, {count}).') ..writeln('Preserve HTML-like or XML-like tags verbatim if present.') ..writeln('Do not introduce new placeholders or variables.') - ..writeln('If translation would be identical to English, ' - 'repeat the English text.') + ..writeln('If translation would be identical to the source, ' + 'repeat the source text.') ..writeln('If a translation is infeasible or unclear, ' - 'copy the English text as fallback.') + 'copy the source text as fallback.') ..writeln('Avoid adding periods if original does not have one; ' 'keep stylistic equivalence.') ..writeln('No leading/trailing spaces in values.') ..writeln('Each value MUST be culturally and medically appropriate, ' 'neutral and concise.') - ..writeln('Keep each translation roughly as long as the English source; ' + ..writeln('Keep each translation roughly as long as the source text; ' 'never pad, repeat or explain.') ..writeln('No quotes escaping beyond standard JSON string escaping.') ..writeln('--- JSON SCHEMA (informal) ---') @@ -146,7 +157,7 @@ import 'language_names.dart'; 'type': 'string', 'minLength': 1, if (name != null) - 'description': 'Translation of en_source into $name ' + 'description': 'Translation of source_text into $name ' '($code), written in the native script of that language.', }, }, diff --git a/pubspec.yaml b/pubspec.yaml index 20aea9d..b0237db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ description: > It uses the Google Sheets API to fetch translations and generates Dart localization files for use in Flutter applications. -version: 0.5.0 +version: 0.6.0 homepage: https://github.com/DoctorinaAI/sheety_localization repository: https://github.com/DoctorinaAI/sheety_localization diff --git a/test/client_test.dart b/test/client_test.dart index 9e092e5..c6d0077 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -150,7 +150,7 @@ void main() { Map schemaFor(List languages) => buildLocalizationPrompt( label: 'greeting', - en: 'Hello', + source: 'Hello', languages: languages, ).schema; diff --git a/test/extract_test.dart b/test/extract_test.dart index 4b15ae8..ff91ef9 100644 --- a/test/extract_test.dart +++ b/test/extract_test.dart @@ -30,7 +30,8 @@ void main() { final greeting = rows.first; expect(greeting.row, 1); - expect(greeting.english, 'Hello'); + expect(greeting.source, 'Hello'); + expect(greeting.sourceCode, 'en'); expect(greeting.description, 'Main screen'); expect(greeting.meta, '{name}'); expect(greeting.cells.map((c) => c.code), ['uk']); @@ -63,7 +64,33 @@ void main() { ['greeting', null, null, null, null, null], ], ); - expect(rows.single.english, 'greeting'); + expect(rows.single.source, 'greeting'); + }); + + test('accepts a non-English source language and carries its code', () { + // The source column is `ru`, not `en`: the sheet must still be + // localized, translating the Russian source into the other columns. + final rows = extractEmptyCells( + title: 'auth', + values: >[ + ['label', 'description', 'meta', 'ru', 'en', 'uk'], + ['greeting', null, null, 'Привет', null, null], + ], + ); + expect(rows.single.sourceCode, 'ru'); + expect(rows.single.source, 'Привет'); + expect(rows.single.cells.map((c) => c.code), ['en', 'uk']); + }); + + test('normalizes the source language code from the header', () { + final rows = extractEmptyCells( + title: 'auth', + values: >[ + ['label', 'description', 'meta', 'pt-BR', 'en'], + ['greeting', null, null, 'Olá', null], + ], + ); + expect(rows.single.sourceCode, 'pt_BR'); }); test('skips malformed rows', () { @@ -151,7 +178,24 @@ void main() { ); }); - test('rejects a sheet whose fourth column is not English', () { + test('accepts any recognized source language, not only English', () { + expect( + isLocalizationHeader( + const ['label', 'description', 'meta', 'ru', 'en'], + ), + isTrue, + ); + expect( + isLocalizationHeader( + const ['label', 'description', 'meta', 'pt-BR', 'en'], + ), + isTrue, + ); + }); + + test('rejects a sheet whose fourth column is not a language', () { + // A reference/data table: `Family` is not a locale, so the sheet must be + // left alone rather than overwritten with translations. expect( isLocalizationHeader( const ['Language', 'Total', 'Native', 'Family', 'Regions'], diff --git a/test/generate_test.dart b/test/generate_test.dart new file mode 100644 index 0000000..d812455 --- /dev/null +++ b/test/generate_test.dart @@ -0,0 +1,45 @@ +// Imports the executable directly (not the package) so its own top-level +// helpers such as `$log` do not clash with the library's. +import '../bin/generate.dart'; +import 'package:test/test.dart'; + +void main() { + group('selectTemplateArb', () { + test('prefers the English ARB when present', () { + expect( + selectTemplateArb( + const ['/l10n/app/app_ru.arb', '/l10n/app/app_en.arb'], + prefix: 'app', + ), + 'app_en.arb', + ); + }); + + test('falls back to the first ARB by name for a non-English base', () { + // A bucket whose base language is Russian: there is no `app_en.arb`, so + // the template must be one of the ARBs that actually exist. + expect( + selectTemplateArb( + const ['/l10n/app/app_uk.arb', '/l10n/app/app_ru.arb'], + prefix: 'app', + ), + 'app_ru.arb', + ); + }); + + test('honours a custom prefix', () { + expect( + selectTemplateArb( + const ['/l10n/app/errors_de.arb', '/l10n/app/errors_en.arb'], + prefix: 'errors', + ), + 'errors_en.arb', + ); + }); + + test('falls back to the conventional English name when there are no ARBs', + () { + expect(selectTemplateArb(const [], prefix: 'app'), 'app_en.arb'); + }); + }); +} diff --git a/test/localizer_test.dart b/test/localizer_test.dart index f721e0c..80f870d 100644 --- a/test/localizer_test.dart +++ b/test/localizer_test.dart @@ -40,7 +40,7 @@ LocalizeRow rowWith(List codes) => LocalizeRow( label: 'greeting', description: null, meta: null, - english: 'Hello', + source: 'Hello', cells: [ for (var i = 0; i < codes.length; i++) LocalizeCell(column: 4 + i, code: codes[i], text: ''), @@ -135,7 +135,7 @@ void main() { test('retries only the language whose translation is invalid', () async { final row = rowWith(['uk', 'ru', 'de']); - row.english = 'Hello, {name}!'; + row.source = 'Hello, {name}!'; var attempt = 0; final client = FakeClient((languages) { attempt++; diff --git a/test/openai_client_http_test.dart b/test/openai_client_http_test.dart index 8da3a50..441da83 100644 --- a/test/openai_client_http_test.dart +++ b/test/openai_client_http_test.dart @@ -89,7 +89,7 @@ void main() { List languages) => buildLocalizationPrompt( label: 'greeting', - en: 'Hello', + source: 'Hello', languages: languages, ); diff --git a/test/prompt_test.dart b/test/prompt_test.dart index 20caeb6..67741ee 100644 --- a/test/prompt_test.dart +++ b/test/prompt_test.dart @@ -5,7 +5,7 @@ void main() => group('buildLocalizationPrompt', () { test('spells out every language by name and endonym', () { final (:prompt, schema: _) = buildLocalizationPrompt( label: 'greeting', - en: 'Hello', + source: 'Hello', languages: const ['uk', 'de'], ); expect(prompt, contains('uk — Ukrainian (українська)')); @@ -15,7 +15,7 @@ void main() => group('buildLocalizationPrompt', () { test('warns the model that uk is not United Kingdom', () { final (:prompt, schema: _) = buildLocalizationPrompt( label: 'greeting', - en: 'Hello', + source: 'Hello', languages: const ['uk'], ); expect(prompt, contains('NOT English')); @@ -27,21 +27,55 @@ void main() => group('buildLocalizationPrompt', () { test('carries context and placeholders into the prompt', () { final (:prompt, schema: _) = buildLocalizationPrompt( label: 'welcome', - en: 'Hello, {name}!', + source: 'Hello, {name}!', description: 'Greeting on the main screen', meta: '{name}: String', languages: const ['ru'], ); expect(prompt, contains('label: welcome')); - expect(prompt, contains('en_source: Hello, {name}!')); + expect(prompt, contains('source_text: Hello, {name}!')); + expect(prompt, contains('source_language: en — English')); expect(prompt, contains('Greeting on the main screen')); expect(prompt, contains('{name}: String')); }); + test('names a non-English source language in the prompt and schema', () { + final (:prompt, :schema) = buildLocalizationPrompt( + label: 'greeting', + source: 'Привет', + sourceCode: 'ru', + languages: const ['de'], + ); + // The source language is spelled out for the model... + expect(prompt, contains('source_language: ru — Russian (русский)')); + expect(prompt, contains('source_text: Привет')); + // ...and the English-specific instructions now follow the source. + expect(prompt, contains('Never answer in Russian for a non-Russian')); + expect(prompt, isNot(contains('Never answer in English'))); + // The target-language schema describes a translation of the source. + final localization = (schema['properties']! + as Map)['localization']! as Map; + final props = localization['properties']! as Map; + final de = props['de']! as Map; + final text = + (de['properties']! as Map)['text']! as Map; + expect(text['description'], contains('source_text into German')); + }); + + test('defaults the source language to English', () { + final (:prompt, schema: _) = buildLocalizationPrompt( + label: 'greeting', + source: 'Hello', + languages: const ['uk'], + ); + expect(prompt, contains('source_language: en — English')); + expect(prompt, contains('Never answer in English for a non-English')); + }); + test('schema requires exactly the requested languages', () { final (prompt: _, :schema) = buildLocalizationPrompt( label: 'greeting', - en: 'Hello', + source: 'Hello', languages: const ['uk', 'ru'], ); final localization = (schema['properties']! @@ -62,7 +96,7 @@ void main() => group('buildLocalizationPrompt', () { test('deduplicates languages, preserving order', () { final (:prompt, :schema) = buildLocalizationPrompt( label: 'greeting', - en: 'Hello', + source: 'Hello', languages: const ['uk', 'ru', 'uk', ' ', 'de'], ); final localization = (schema['properties']! @@ -75,7 +109,7 @@ void main() => group('buildLocalizationPrompt', () { expect( () => buildLocalizationPrompt( label: ' ', - en: 'Hello', + source: 'Hello', languages: const ['uk'], ), throwsArgumentError, @@ -83,7 +117,7 @@ void main() => group('buildLocalizationPrompt', () { expect( () => buildLocalizationPrompt( label: 'greeting', - en: '', + source: '', languages: const ['uk'], ), throwsArgumentError, @@ -91,7 +125,7 @@ void main() => group('buildLocalizationPrompt', () { expect( () => buildLocalizationPrompt( label: 'greeting', - en: 'Hello', + source: 'Hello', languages: const [], ), throwsArgumentError, diff --git a/test/sheets_test.dart b/test/sheets_test.dart index fdc434c..5de2dc4 100644 --- a/test/sheets_test.dart +++ b/test/sheets_test.dart @@ -29,7 +29,7 @@ LocalizeRow rowWith(Map cells, {int row = 4}) => LocalizeRow( label: 'greeting', description: null, meta: null, - english: 'Hello', + source: 'Hello', cells: [ for (final (index, MapEntry(:key, :value)) in cells.entries.indexed.toList())